3 # This file is part of Koha.
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.
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.
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>.
20 use Test::More tests => 105;
24 use t::lib::TestBuilder;
29 use C4::Circulation qw( AddIssue AddReturn CanBookBeIssued );
32 use Koha::DateUtils qw( dt_from_string output_pref );
35 my $schema = Koha::Database->schema;
36 my $builder = t::lib::TestBuilder->new;
38 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
39 my $t = Test::Mojo->new('Koha::REST::V1');
41 $schema->storage->txn_begin;
43 my $dbh = C4::Context->dbh;
45 my $librarian = $builder->build_object({
46 class => 'Koha::Patrons',
47 value => { flags => 2 }
49 my $password = 'thePassword123';
50 $librarian->set_password({ password => $password, skip_validation => 1 });
51 my $userid = $librarian->userid;
53 my $patron = $builder->build_object({
54 class => 'Koha::Patrons',
55 value => { flags => 0 }
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;
62 my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
64 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
68 my $notexisting_patron_id = $patron_id + 1;
69 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$notexisting_patron_id" )
73 Koha::CirculationRules->set_rules(
75 categorycode => undef,
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;
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);
101 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
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 }) )
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" )
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 }) )
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" )
125 ->json_is('/0/patron_id' => $patron_id)
126 ->json_is('/0/item_id' => $item4->itemnumber)
130 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=1" )
132 ->json_is('/0/patron_id' => $patron_id)
133 ->json_is('/0/item_id' => undef);
135 $t->get_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id )
137 ->json_is({ error => "Authorization failure. Missing required permission(s).",
138 required_permissions => { circulate => "circulate_remaining_permissions" }
141 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id")
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 }) )
151 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=1")
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 }) )
162 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=2")
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 }) )
173 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
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 }) )
180 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
182 ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) );
184 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id)
186 ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $date_due2 }) );
188 my $expected_datedue = $date_due
189 ->set_time_zone('local')
191 ->set(hour => 23, minute => 59, second => 0);
193 $t->post_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
195 ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $expected_datedue }) )
196 ->header_is(Location => "/api/v1/checkouts/" . $issue1->issue_id . "/renewal");
198 my $renewal = $issue1->renewals->last;
199 is( $renewal->renewal_type, 'Manual', 'Manual renewal recorded' );
201 $t->get_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewals" )
203 ->json_is('/0/checkout_id' => $issue1->issue_id)
204 ->json_is('/0/interface' => 'api')
205 ->json_is('/0/renewer_id' => $librarian->borrowernumber );
207 $t->post_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id . "/renewal" )
209 ->json_is({ error => "Authorization failure. Missing required permission(s).",
210 required_permissions => { circulate => "circulate_remaining_permissions" }
213 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
216 allows_renewal => Mojo::JSON->true,
218 unseen_renewals => 0,
219 current_renewals => 0,
223 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/renewal" )
225 ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $expected_datedue}) )
226 ->header_is(Location => "/api/v1/checkouts/" . $issue2->issue_id . "/renewal");
229 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
231 ->json_is({ error => 'Renewal not authorized (too_many)' });
233 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
236 allows_renewal => Mojo::JSON->false,
238 unseen_renewals => 0,
239 current_renewals => 1,
243 $schema->storage->txn_rollback;
245 subtest 'get_availability' => sub {
249 $schema->storage->txn_begin;
250 my $librarian = $builder->build_object(
252 class => 'Koha::Patrons',
253 value => { flags => 2 }
256 my $password = 'thePassword123';
257 $librarian->set_password( { password => $password, skip_validation => 1 } );
258 my $userid = $librarian->userid;
260 my $patron = $builder->build_object(
262 class => 'Koha::Patrons',
263 value => { flags => 0 }
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;
271 my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
273 my $item1 = $builder->build_sample_item;
274 my $item1_id = $item1->id;
276 my %issuingimpossible = ();
277 my %needsconfirmation = ();
280 my $mocked_circ = Test::MockModule->new('C4::Circulation');
284 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
289 "//$unauth_userid:$unauth_password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")
290 ->status_is(403)->json_is(
292 error => "Authorization failure. Missing required permission(s).",
293 required_permissions => { circulate => "circulate_remaining_permissions" }
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');
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 = ();
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');
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');
328 Koha::Token->new->check_jwt(
330 id => $librarian->id . ":" . $item1_id . ":confirm1:confirm2",
331 token => $confirmation_token
336 %needsconfirmation = ();
338 subtest 'public availability' => sub {
341 # Authentication required
342 $t->get_ok("/api/v1/public/checkouts/availability?item_id=$item1_id&patron_id=$patron_id")->status_is(401);
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")
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');
354 # Needs confirmation upgrade to blocker
355 %needsconfirmation = ( TOO_MANY => 1, ISSUED_TO_ANOTHER => 1 );
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 = ();
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'
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'
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'
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 = ();
412 %needsconfirmation = ();
415 $schema->storage->txn_rollback;
418 subtest 'add checkout' => sub {
422 $schema->storage->txn_begin;
423 my $librarian = $builder->build_object(
425 class => 'Koha::Patrons',
426 value => { flags => 2 }
429 my $password = 'thePassword123';
430 $librarian->set_password( { password => $password, skip_validation => 1 } );
431 my $userid = $librarian->userid;
433 my $patron = $builder->build_object(
435 class => 'Koha::Patrons',
436 value => { flags => 0 }
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;
444 my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
446 my $item1 = $builder->build_sample_item;
447 my $item1_id = $item1->id;
449 my %issuingimpossible = ();
450 my %needsconfirmation = ();
453 my $mocked_circ = Test::MockModule->new('C4::Circulation');
457 return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
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(
464 error => "Authorization failure. Missing required permission(s).",
465 required_permissions => { circulate => "circulate_remaining_permissions" }
469 $t->post_ok( "//$userid:$password@/api/v1/checkouts" => json => { item_id => $item1_id, patron_id => $patron_id } )
473 %needsconfirmation = ( confirm1 => 1, confirm2 => 'please' );
475 "//$userid:$password@/api/v1/checkouts" => json => {
476 item_id => $item1_id,
477 patron_id => $patron_id,
481 my $token = Koha::Token->new->generate_jwt( { id => $librarian->id . ":" . $item1_id . ":confirm1:confirm2" } );
483 "//$userid:$password@/api/v1/checkouts?confirmation=$token" => json => {
484 item_id => $item1_id,
485 patron_id => $patron_id
487 )->status_is(201)->or( sub { diag $t->tx->res->body } );
488 %needsconfirmation = ();
490 subtest 'public add' => sub {
493 my $useridp = $patron->userid;
494 $patron->set_password( { password => $password, skip_validation => 1 } );
497 t::lib::Mocks::mock_preference( 'OpacTrustedCheckout', 0 );
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." } );
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" } );
508 t::lib::Mocks::mock_preference( 'OpacTrustedCheckout', 1 );
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." } );
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(
517 error => "Authorization failure. Missing required permission(s).",
518 required_permissions => undef
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);
526 $schema->storage->txn_rollback;