]> git.koha-community.org Git - koha.git/blob - t/db_dependent/api/v1/checkouts.t
Bug 34287: Add check on public availability endpoint
[koha.git] / t / db_dependent / api / v1 / checkouts.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
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 use Test::More tests => 105;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::Mocks;
24 use t::lib::TestBuilder;
25
26 use DateTime;
27
28 use C4::Context;
29 use C4::Circulation qw( AddIssue AddReturn CanBookBeIssued );
30
31 use Koha::Database;
32 use Koha::DateUtils qw( dt_from_string output_pref );
33 use Koha::Token;
34
35 my $schema = Koha::Database->schema;
36 my $builder = t::lib::TestBuilder->new;
37
38 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
39 my $t = Test::Mojo->new('Koha::REST::V1');
40
41 $schema->storage->txn_begin;
42
43 my $dbh = C4::Context->dbh;
44
45 my $librarian = $builder->build_object({
46     class => 'Koha::Patrons',
47     value => { flags => 2 }
48 });
49 my $password = 'thePassword123';
50 $librarian->set_password({ password => $password, skip_validation => 1 });
51 my $userid = $librarian->userid;
52
53 my $patron = $builder->build_object({
54     class => 'Koha::Patrons',
55     value => { flags => 0 }
56 });
57 my $unauth_password = 'thePassword000';
58 $patron->set_password({ password => $unauth_password, skip_validattion => 1 });
59 my $unauth_userid = $patron->userid;
60 my $patron_id = $patron->borrowernumber;
61
62 my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
63
64 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
65   ->status_is(200)
66   ->json_is([]);
67
68 my $notexisting_patron_id = $patron_id + 1;
69 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$notexisting_patron_id" )
70   ->status_is(200)
71   ->json_is([]);
72
73 Koha::CirculationRules->set_rules(
74     {
75         categorycode => undef,
76         itemtype     => undef,
77         branchcode   => undef,
78         rules        => {
79             renewalperiod => 7,
80             renewalsallowed => 1,
81             issuelength => 5,
82         }
83     }
84 );
85
86 my $item1 = $builder->build_sample_item;
87 my $item2 = $builder->build_sample_item;
88 my $item3 = $builder->build_sample_item;
89 my $item4 = $builder->build_sample_item;
90
91 my $date_due = DateTime->now->add(weeks => 2);
92 my $issue1 = C4::Circulation::AddIssue($patron, $item1->barcode, $date_due);
93 my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due );
94 my $issue2 = C4::Circulation::AddIssue($patron, $item2->barcode, $date_due);
95 my $date_due2 = Koha::DateUtils::dt_from_string( $issue2->date_due );
96 my $issue3 = C4::Circulation::AddIssue($librarian, $item3->barcode, $date_due);
97 my $date_due3 = Koha::DateUtils::dt_from_string( $issue3->date_due );
98 my $issue4 = C4::Circulation::AddIssue($patron, $item4->barcode);
99 C4::Circulation::AddReturn($item4->barcode, $branchcode);
100
101 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
102   ->status_is(200)
103   ->json_is('/0/patron_id' => $patron_id)
104   ->json_is('/0/item_id' => $item1->itemnumber)
105   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
106   ->json_is('/1/patron_id' => $patron_id)
107   ->json_is('/1/item_id' => $item2->itemnumber)
108   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
109   ->json_hasnt('/2');
110
111 # Test checked_in parameter, zero means, the response is same as without it
112 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=0" )
113   ->status_is(200)
114   ->json_is('/0/patron_id' => $patron_id)
115   ->json_is('/0/item_id' => $item1->itemnumber)
116   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
117   ->json_is('/1/patron_id' => $patron_id)
118   ->json_is('/1/item_id' => $item2->itemnumber)
119   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
120   ->json_hasnt('/2');
121
122 # Test checked_in parameter, one measn, the checked in checkout is in the response too
123 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=1" )
124   ->status_is(200)
125   ->json_is('/0/patron_id' => $patron_id)
126   ->json_is('/0/item_id' => $item4->itemnumber)
127   ->json_hasnt('/1');
128
129 $item4->delete;
130 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=1" )
131   ->status_is(200)
132   ->json_is('/0/patron_id' => $patron_id)
133   ->json_is('/0/item_id' => undef);
134
135 $t->get_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id )
136   ->status_is(403)
137   ->json_is({ error => "Authorization failure. Missing required permission(s).",
138               required_permissions => { circulate => "circulate_remaining_permissions" }
139             });
140
141 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id")
142   ->status_is(200)
143   ->json_is('/0/patron_id' => $patron_id)
144   ->json_is('/0/item_id' => $item1->itemnumber)
145   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
146   ->json_is('/1/patron_id' => $patron_id)
147   ->json_is('/1/item_id' => $item2->itemnumber)
148   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
149   ->json_hasnt('/2');
150
151 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=1")
152   ->status_is(200)
153   ->header_is('X-Total-Count', '2')
154   ->header_like('Link', qr|rel="next"|)
155   ->header_like('Link', qr|rel="first"|)
156   ->header_like('Link', qr|rel="last"|)
157   ->json_is('/0/patron_id' => $patron_id)
158   ->json_is('/0/item_id' => $item1->itemnumber)
159   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
160   ->json_hasnt('/1');
161
162 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=2")
163   ->status_is(200)
164   ->header_is('X-Total-Count', '2')
165   ->header_like('Link', qr|rel="prev"|)
166   ->header_like('Link', qr|rel="first"|)
167   ->header_like('Link', qr|rel="last"|)
168   ->json_is('/0/patron_id' => $patron_id)
169   ->json_is('/0/item_id' => $item2->itemnumber)
170   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
171   ->json_hasnt('/1');
172
173 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
174   ->status_is(200)
175   ->json_is('/patron_id' => $patron_id)
176   ->json_is('/item_id' => $item1->itemnumber)
177   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
178   ->json_hasnt('/1');
179
180 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
181   ->status_is(200)
182   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) );
183
184 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id)
185   ->status_is(200)
186   ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $date_due2 }) );
187
188 my $expected_datedue = $date_due
189     ->set_time_zone('local')
190     ->add(days => 7)
191     ->set(hour => 23, minute => 59, second => 0);
192
193 $t->post_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
194   ->status_is(201)
195   ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $expected_datedue }) )
196   ->header_is(Location => "/api/v1/checkouts/" . $issue1->issue_id . "/renewal");
197
198 my $renewal = $issue1->renewals->last;
199 is( $renewal->renewal_type, 'Manual', 'Manual renewal recorded' );
200
201 $t->get_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewals" )
202   ->status_is(200)
203   ->json_is('/0/checkout_id' => $issue1->issue_id)
204   ->json_is('/0/interface'   => 'api')
205   ->json_is('/0/renewer_id'  => $librarian->borrowernumber );
206
207 $t->post_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id . "/renewal" )
208   ->status_is(403)
209   ->json_is({ error => "Authorization failure. Missing required permission(s).",
210               required_permissions => { circulate => "circulate_remaining_permissions" }
211             });
212
213 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
214   ->status_is(200)
215   ->json_is({
216         allows_renewal   => Mojo::JSON->true,
217         max_renewals     => 1,
218         unseen_renewals  => 0,
219         current_renewals => 0,
220         error            => undef
221     });
222
223 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/renewal" )
224   ->status_is(201)
225   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $expected_datedue}) )
226   ->header_is(Location => "/api/v1/checkouts/" . $issue2->issue_id . "/renewal");
227
228
229 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
230   ->status_is(403)
231   ->json_is({ error => 'Renewal not authorized (too_many)' });
232
233 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
234   ->status_is(200)
235   ->json_is({
236         allows_renewal   => Mojo::JSON->false,
237         max_renewals     => 1,
238         unseen_renewals  => 0,
239         current_renewals => 1,
240         error            => 'too_many'
241     });
242
243 $schema->storage->txn_rollback;
244
245 subtest 'get_availability' => sub {
246
247     plan tests => 29;
248
249     $schema->storage->txn_begin;
250     my $librarian = $builder->build_object(
251         {
252             class => 'Koha::Patrons',
253             value => { flags => 2 }
254         }
255     );
256     my $password = 'thePassword123';
257     $librarian->set_password( { password => $password, skip_validation => 1 } );
258     my $userid = $librarian->userid;
259
260     my $patron = $builder->build_object(
261         {
262             class => 'Koha::Patrons',
263             value => { flags => 0 }
264         }
265     );
266     my $unauth_password = 'thePassword000';
267     $patron->set_password( { password => $unauth_password, skip_validattion => 1 } );
268     my $unauth_userid = $patron->userid;
269     my $patron_id     = $patron->borrowernumber;
270
271     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
272
273     my $item1    = $builder->build_sample_item;
274     my $item1_id = $item1->id;
275
276     my %issuingimpossible = ();
277     my %needsconfirmation = ();
278     my %alerts            = ();
279     my %messages          = ();
280     my $mocked_circ       = Test::MockModule->new('C4::Circulation');
281     $mocked_circ->mock(
282         'CanBookBeIssued',
283         sub {
284             return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
285         }
286     );
287
288     $t->get_ok(
289         "//$unauth_userid:$unauth_password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
290         ->status_is(403)->json_is(
291         {
292             error                => "Authorization failure. Missing required permission(s).",
293             required_permissions => { circulate => "circulate_remaining_permissions" }
294         }
295         );
296
297     # Available
298     $t->get_ok("//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
299         ->status_is(200)->json_is( '/blockers' => {} )->json_is( '/confirms' => {} )->json_is( '/warnings' => {} )
300         ->json_has('/confirmation_token');
301
302     # Blocked
303     %issuingimpossible = ( GNA => 1 );
304     $t->get_ok("//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
305         ->status_is(200)->json_is( '/blockers' => { GNA => 1 } )->json_is( '/confirms' => {} )
306         ->json_is( '/warnings' => {} )->json_has('/confirmation_token');
307     %issuingimpossible = ();
308
309     # Warnings/Info
310     %alerts   = ( alert1   => "this is an alert" );
311     %messages = ( message1 => "this is a message" );
312     $t->get_ok("//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
313         ->status_is(200)->json_is( '/blockers' => {} )->json_is( '/confirms' => {} )
314         ->json_is( '/warnings' => { alert1 => "this is an alert", message1 => "this is a message" } )
315         ->json_has('/confirmation_token');
316     %alerts   = ();
317     %messages = ();
318
319     # Needs confirm
320     %needsconfirmation = ( confirm1 => 1, confirm2 => 'please' );
321     my $token = Koha::Token->new->generate_jwt( { id => $librarian->id . ":" . $item1_id . ":confirm1:confirm2" } );
322     $t->get_ok("//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
323         ->status_is(200)->json_is( '/blockers' => {} )
324         ->json_is( '/confirms' => { confirm1 => 1, confirm2 => 'please' } )->json_is( '/warnings' => {} )
325         ->json_has('/confirmation_token');
326     my $confirmation_token = $t->tx->res->json('/confirmation_token');
327     ok(
328         Koha::Token->new->check_jwt(
329             {
330                 id    => $librarian->id . ":" . $item1_id . ":confirm1:confirm2",
331                 token => $confirmation_token
332             }
333         ),
334         'Correct token'
335     );
336     %needsconfirmation = ();
337
338     subtest 'public availability' => sub {
339         plan tests => 22;
340
341         # Authentication required
342         $t->get_ok("/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")->status_is(401);
343
344         # Only allow availability lookup for self
345         $t->get_ok("//$userid:$password@/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
346             ->status_is(403);
347
348         # All ok
349         $t->get_ok(
350             "//$unauth_userid:$unauth_password@/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
351         )->status_is(200)->json_is( '/blockers' => {} )->json_is( '/confirms' => {} )->json_is( '/warnings' => {} )
352             ->json_has('/confirmation_token');
353
354         # Needs confirmation upgrade to blocker
355         %needsconfirmation = ( TOO_MANY => 1, ISSUED_TO_ANOTHER => 1 );
356         $t->get_ok(
357             "//$unauth_userid:$unauth_password@/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
358         )->status_is(200)->json_is( '/blockers' => { TOO_MANY => 1, ISSUED_TO_ANOTHER => 1 } )
359             ->json_is( '/confirms' => {} )->json_is( '/warnings' => {} )->json_has('/confirmation_token');
360         %needsconfirmation = ();
361
362         # Remove personal information from public endpoint
363         %issuingimpossible = (
364             issued_borrowernumber => 'private',
365             issued_cardnumber     => 'private',
366             issued_firstname      => 'private',
367             issued_surname        => 'private',
368             resborrowernumber     => 'private',
369             resbranchcode         => 'private',
370             rescardnumber         => 'private',
371             reserve_id            => 'private',
372             resfirstname          => 'private',
373             resreservedate        => 'private',
374             ressurname            => 'private',
375             item_notforloan       => 'private'
376         );
377         %alerts = (
378             issued_borrowernumber => 'private',
379             issued_cardnumber     => 'private',
380             issued_firstname      => 'private',
381             issued_surname        => 'private',
382             resborrowernumber     => 'private',
383             resbranchcode         => 'private',
384             rescardnumber         => 'private',
385             reserve_id            => 'private',
386             resfirstname          => 'private',
387             resreservedate        => 'private',
388             ressurname            => 'private',
389             item_notforloan       => 'private'
390         );
391
392         %needsconfirmation = (
393             issued_borrowernumber => 'private',
394             issued_cardnumber     => 'private',
395             issued_firstname      => 'private',
396             issued_surname        => 'private',
397             resborrowernumber     => 'private',
398             resbranchcode         => 'private',
399             rescardnumber         => 'private',
400             reserve_id            => 'private',
401             resfirstname          => 'private',
402             resreservedate        => 'private',
403             ressurname            => 'private',
404             item_notforloan       => 'private'
405         );
406         $t->get_ok(
407             "//$unauth_userid:$unauth_password@/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
408         )->status_is(200)->json_is( '/blockers' => {} )->json_is( '/confirms' => {} )->json_is( '/warnings' => {} )
409             ->json_has('/confirmation_token');
410         %issuingimpossible = ();
411         %alerts            = ();
412         %needsconfirmation = ();
413     };
414
415     $schema->storage->txn_rollback;
416 };
417
418 subtest 'add checkout' => sub {
419
420     plan tests => 10;
421
422     $schema->storage->txn_begin;
423     my $librarian = $builder->build_object(
424         {
425             class => 'Koha::Patrons',
426             value => { flags => 2 }
427         }
428     );
429     my $password = 'thePassword123';
430     $librarian->set_password( { password => $password, skip_validation => 1 } );
431     my $userid = $librarian->userid;
432
433     my $patron = $builder->build_object(
434         {
435             class => 'Koha::Patrons',
436             value => { flags => 0 }
437         }
438     );
439     my $unauth_password = 'thePassword000';
440     $patron->set_password( { password => $unauth_password, skip_validattion => 1 } );
441     my $unauth_userid = $patron->userid;
442     my $patron_id     = $patron->borrowernumber;
443
444     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
445
446     my $item1    = $builder->build_sample_item;
447     my $item1_id = $item1->id;
448
449     my %issuingimpossible = ();
450     my %needsconfirmation = ();
451     my %alerts            = ();
452     my %messages          = ();
453     my $mocked_circ       = Test::MockModule->new('C4::Circulation');
454     $mocked_circ->mock(
455         'CanBookBeIssued',
456         sub {
457             return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
458         }
459     );
460
461     $t->post_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts" => json =>
462             { item_id => $item1_id, patron_id => $patron_id } )->status_is(403)->json_is(
463         {
464             error                => "Authorization failure. Missing required permission(s).",
465             required_permissions => { circulate => "circulate_remaining_permissions" }
466         }
467             );
468
469     $t->post_ok( "//$userid:$password@/api/v1/checkouts" => json => { item_id => $item1_id, patron_id => $patron_id } )
470         ->status_is(201);
471
472     # Needs confirm
473     %needsconfirmation = ( confirm1 => 1, confirm2 => 'please' );
474     $t->post_ok(
475         "//$userid:$password@/api/v1/checkouts" => json => {
476             item_id   => $item1_id,
477             patron_id => $patron_id,
478         }
479     )->status_is(412);
480
481     my $token = Koha::Token->new->generate_jwt( { id => $librarian->id . ":" . $item1_id . ":confirm1:confirm2" } );
482     $t->post_ok(
483         "//$userid:$password@/api/v1/checkouts?confirmation=$token" => json => {
484             item_id   => $item1_id,
485             patron_id => $patron_id
486         }
487     )->status_is(201)->or( sub { diag $t->tx->res->body } );
488     %needsconfirmation = ();
489
490     subtest 'public add' => sub {
491         plan tests => 14;
492
493         my $useridp = $patron->userid;
494         $patron->set_password( { password => $password, skip_validation => 1 } );
495
496         # Feature disabled
497         t::lib::Mocks::mock_preference( 'OpacTrustedCheckout', 0 );
498
499         $t->post_ok(
500             "/api/v1/public/patrons/$patron_id/checkouts" => json => { item_id => $item1_id, patron_id => $patron_id } )
501             ->status_is(401)->json_is( { error => "Authentication failure." } );
502
503         $t->post_ok( "//$useridp:$password@/api/v1/public/patrons/$patron_id/checkouts" => json =>
504                 { item_id => $item1_id, patron_id => $patron_id } )->status_is(405)
505             ->json_is( { error => "Feature disabled", error_code => "FEATURE_DISABLED" } );
506
507         # Feature enabled
508         t::lib::Mocks::mock_preference( 'OpacTrustedCheckout', 1 );
509
510         $t->post_ok(
511             "/api/v1/public/patrons/$patron_id/checkouts" => json => { item_id => $item1_id, patron_id => $patron_id } )
512             ->status_is(401)->json_is( { error => "Authentication failure." } );
513
514         $t->post_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/checkouts" => json =>
515                 { item_id => $item1_id, patron_id => $patron_id } )->status_is(403)->json_is(
516             {
517                 error                => "Authorization failure. Missing required permission(s).",
518                 required_permissions => undef
519             }
520                 );
521
522         $t->post_ok( "//$useridp:$password@/api/v1/public/patrons/$patron_id/checkouts" => json =>
523                 { item_id => $item1_id, patron_id => $patron_id } )->status_is(201);
524     };
525
526     $schema->storage->txn_rollback;
527 };