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>.
22 use Test::More tests => 10;
25 use t::lib::TestBuilder;
27 use C4::Items qw( ModItemTransfer );
34 use_ok('C4::ILSDI::Services');
37 my $schema = Koha::Database->schema;
38 my $dbh = C4::Context->dbh;
39 my $builder = t::lib::TestBuilder->new;
41 subtest 'AuthenticatePatron test' => sub {
45 $schema->storage->txn_begin;
47 my $plain_password = 'tomasito';
56 my $borrower = $builder->build({
60 password => Koha::AuthUtils::hash_password( $plain_password ),
61 lastseen => "2001-01-01"
66 $query->param( 'username', $borrower->{userid});
67 $query->param( 'password', $plain_password);
69 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '' );
70 my $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
71 is( $reply->{id}, $borrower->{borrowernumber}, "userid and password - Patron authenticated" );
72 is( $reply->{code}, undef, "Error code undef");
73 my $seen_patron = Koha::Patrons->find({ borrowernumber => $reply->{id} });
74 is( output_pref({str => $seen_patron->lastseen(), dateonly => 1}), output_pref({str => '2001-01-01', dateonly => 1}),'Last seen not updated if not tracking patrons');
76 $query->param('password','ilsdi-passworD');
77 $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
78 is( $reply->{code}, 'PatronNotFound', "userid and wrong password - PatronNotFound" );
79 is( $reply->{id}, undef, "id undef");
81 $query->param( 'password', $plain_password );
82 $query->param( 'username', 'wrong-ilsdi-useriD' );
83 $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
84 is( $reply->{code}, 'PatronNotFound', "non-existing userid - PatronNotFound" );
85 is( $reply->{id}, undef, "id undef");
87 t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
88 $query->param( 'username', uc( $borrower->{userid} ));
89 $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
90 is( $reply->{id}, $borrower->{borrowernumber}, "userid is not case sensitive - Patron authenticated" );
91 is( $reply->{code}, undef, "Error code undef");
92 $seen_patron = Koha::Patrons->find({ borrowernumber => $reply->{id} });
93 is( output_pref({str => $seen_patron->lastseen(), dateonly => 1}), output_pref({dt => dt_from_string(), dateonly => 1}),'Last seen updated to today if tracking patrons');
95 $query->param( 'username', $borrower->{cardnumber} );
96 $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
97 is( $reply->{id}, $borrower->{borrowernumber}, "cardnumber and password - Patron authenticated" );
98 is( $reply->{code}, undef, "Error code undef" );
100 $query->param( 'password', 'ilsdi-passworD' );
101 $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
102 is( $reply->{code}, 'PatronNotFound', "cardnumber and wrong password - PatronNotFount" );
103 is( $reply->{id}, undef, "id undef" );
105 $query->param( 'username', 'randomcardnumber1234' );
106 $query->param( 'password', $plain_password );
107 $reply = C4::ILSDI::Services::AuthenticatePatron($query);
108 is( $reply->{code}, 'PatronNotFound', "non-existing cardnumer/userid - PatronNotFound" );
109 is( $reply->{id}, undef, "id undef");
111 $schema->storage->txn_rollback;
115 subtest 'GetPatronInfo/GetBorrowerAttributes test for extended patron attributes' => sub {
119 $schema->storage->txn_begin;
121 $schema->resultset( 'Issue' )->delete_all;
122 $schema->resultset( 'Borrower' )->delete_all;
123 $schema->resultset( 'BorrowerAttribute' )->delete_all;
124 $schema->resultset( 'BorrowerAttributeType' )->delete_all;
125 $schema->resultset( 'Category' )->delete_all;
126 $schema->resultset( 'Item' )->delete_all; # 'Branch' deps. on this
127 $schema->resultset( 'Club' )->delete_all;
128 $schema->resultset( 'Branch' )->delete_all;
130 # Configure Koha to enable ILS-DI server and extended attributes:
131 t::lib::Mocks::mock_preference( 'ILS-DI', 1 );
132 t::lib::Mocks::mock_preference( 'ExtendedPatronAttributes', 1 );
134 # Set up a library/branch for our user to belong to:
135 my $lib = $builder->build( {
138 branchcode => 'T_ILSDI',
142 # Create a new category for user to belong to:
143 my $cat = $builder->build( {
144 source => 'Category',
146 category_type => 'A',
147 BlockExpiredPatronOpacActions => -1,
151 # Create a new attribute type:
152 my $attr_type = $builder->build( {
153 source => 'BorrowerAttributeType',
157 authorised_value_category => '',
161 my $attr_type_visible = $builder->build( {
162 source => 'BorrowerAttributeType',
166 authorised_value_category => '',
172 my $brwr = $builder->build( {
173 source => 'Borrower',
175 categorycode => $cat->{'categorycode'},
176 branchcode => $lib->{'branchcode'},
181 my $auth = $builder->build( {
182 source => 'AuthorisedValue',
184 category => $cat->{'categorycode'}
188 # Set the new attribute for our user:
189 my $attr_hidden = $builder->build( {
190 source => 'BorrowerAttribute',
192 borrowernumber => $brwr->{'borrowernumber'},
193 code => $attr_type->{'code'},
194 attribute => '1337 hidden',
197 my $attr_shown = $builder->build( {
198 source => 'BorrowerAttribute',
200 borrowernumber => $brwr->{'borrowernumber'},
201 code => $attr_type_visible->{'code'},
202 attribute => '1337 shown',
206 my $fine = $builder->build(
208 source => 'Accountline',
210 borrowernumber => $brwr->{borrowernumber},
211 debit_type_code => 'OVERDUE',
212 amountoutstanding => 10
217 # Prepare and send web request for IL-SDI server:
218 my $query = CGI->new;
219 $query->param( 'service', 'GetPatronInfo' );
220 $query->param( 'patron_id', $brwr->{'borrowernumber'} );
221 $query->param( 'show_attributes', '1' );
222 $query->param( 'show_fines', '1' );
224 my $reply = C4::ILSDI::Services::GetPatronInfo( $query );
226 # Build a structure for comparison:
228 borrowernumber => $brwr->{borrowernumber},
229 value => $attr_shown->{'attribute'},
230 value_description => $attr_shown->{'attribute'},
235 is( $reply->{'charges'}, '10.00',
236 'The \'charges\' attribute should be correctly filled (bug 17836)' );
238 is( scalar( @{$reply->{fines}->{fine}}), 1, 'There should be only 1 account line');
240 $reply->{fines}->{fine}->[0]->{accountlines_id},
241 $fine->{accountlines_id},
242 "The accountline should be the correct one"
246 is_deeply( $reply->{'attributes'}, [ $cmp ], 'Test GetPatronInfo - show_attributes parameter' );
248 ok( exists $reply->{is_expired}, 'There should be the is_expired information');
251 $schema->storage->txn_rollback;
254 subtest 'LookupPatron test' => sub {
258 $schema->storage->txn_begin;
260 $schema->resultset( 'Issue' )->delete_all;
261 $schema->resultset( 'Borrower' )->delete_all;
262 $schema->resultset( 'BorrowerAttribute' )->delete_all;
263 $schema->resultset( 'BorrowerAttributeType' )->delete_all;
264 $schema->resultset( 'Category' )->delete_all;
265 $schema->resultset( 'Item' )->delete_all; # 'Branch' deps. on this
266 $schema->resultset( 'Branch' )->delete_all;
268 my $borrower = $builder->build({
269 source => 'Borrower',
272 my $query = CGI->new();
273 my $bad_result = C4::ILSDI::Services::LookupPatron($query);
274 is( $bad_result->{message}, 'PatronNotFound', 'No parameters' );
276 $query->delete_all();
277 $query->param( 'id', $borrower->{firstname} );
278 my $optional_result = C4::ILSDI::Services::LookupPatron($query);
280 $optional_result->{id},
281 $borrower->{borrowernumber},
282 'Valid Firstname only'
285 $query->delete_all();
286 $query->param( 'id', 'ThereIsNoWayThatThisCouldPossiblyBeValid' );
287 my $bad_optional_result = C4::ILSDI::Services::LookupPatron($query);
288 is( $bad_optional_result->{message}, 'PatronNotFound', 'Invalid ID' );
290 foreach my $id_type (
298 $query->delete_all();
299 $query->param( 'id_type', $id_type );
300 $query->param( 'id', $borrower->{$id_type} );
301 my $result = C4::ILSDI::Services::LookupPatron($query);
302 is( $result->{'id'}, $borrower->{borrowernumber}, "Checking $id_type" );
306 $schema->storage->txn_rollback;
309 subtest 'Holds test' => sub {
313 $schema->storage->txn_begin;
315 t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 0 );
317 my $patron = $builder->build({
318 source => 'Borrower',
321 my $item = $builder->build_sample_item(
327 my $query = CGI->new;
328 $query->param( 'patron_id', $patron->{borrowernumber});
329 $query->param( 'bib_id', $item->biblionumber);
331 my $reply = C4::ILSDI::Services::HoldTitle( $query );
332 is( $reply->{code}, 'damaged', "Item damaged" );
334 $item->damaged(0)->store;
336 my $hold = $builder->build({
339 borrowernumber => $patron->{borrowernumber},
340 biblionumber => $item->biblionumber,
341 itemnumber => $item->itemnumber
345 $reply = C4::ILSDI::Services::HoldTitle( $query );
346 is( $reply->{code}, 'itemAlreadyOnHold', "Item already on hold" );
348 my $biblio_with_no_item = $builder->build_sample_biblio;
351 $query->param( 'patron_id', $patron->{borrowernumber});
352 $query->param( 'bib_id', $biblio_with_no_item->biblionumber);
354 $reply = C4::ILSDI::Services::HoldTitle( $query );
355 is( $reply->{code}, 'NoItems', 'Biblio has no item' );
357 my $item2 = $builder->build_sample_item(
363 t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
364 Koha::CirculationRules->set_rule(
366 categorycode => $patron->{categorycode},
367 itemtype => $item2->{itype},
368 branchcode => $patron->{branchcode},
369 rule_name => 'reservesallowed',
375 $query->param( 'patron_id', $patron->{borrowernumber});
376 $query->param( 'bib_id', $item2->biblionumber);
377 $query->param( 'item_id', $item2->itemnumber);
379 $reply = C4::ILSDI::Services::HoldItem( $query );
380 is( $reply->{code}, 'tooManyReserves', "Too many reserves" );
382 Koha::CirculationRules->set_rule(
384 categorycode => $patron->{categorycode},
385 itemtype => $item2->{itype},
386 branchcode => $patron->{branchcode},
387 rule_name => 'reservesallowed',
393 $query->param( 'patron_id', $patron->{borrowernumber});
394 $query->param( 'bib_id', $item2->biblionumber);
395 $query->param( 'item_id', $item2->itemnumber);
397 $reply = C4::ILSDI::Services::HoldItem( $query );
398 is( $reply->{code}, 'noReservesAllowed', "No reserves allowed" );
400 my $origin_branch = $builder->build(
404 pickup_location => 1,
409 # Adding a holdable item.
410 my $item3 = $builder->build_sample_item(
412 barcode => '123456789',
413 library => $origin_branch->{branchcode}
416 my $item4 = $builder->build_sample_item(
418 biblionumber => $item3->biblionumber,
420 library => $origin_branch->{branchcode}
423 Koha::CirculationRules->set_rule(
425 categorycode => $patron->{categorycode},
426 itemtype => $item3->{itype},
427 branchcode => $patron->{branchcode},
428 rule_name => 'reservesallowed',
434 $query->param( 'patron_id', $patron->{borrowernumber});
435 $query->param( 'bib_id', $item4->biblionumber);
436 $query->param( 'item_id', $item4->itemnumber);
438 $reply = C4::ILSDI::Services::HoldItem( $query );
439 is( $reply->{code}, 'damaged', "Item is damaged" );
441 my $module = Test::MockModule->new('C4::Context');
442 $module->mock('userenv', sub { { patron => $patron } });
443 my $issue = C4::Circulation::AddIssue($patron, $item3->barcode);
444 t::lib::Mocks::mock_preference( 'AllowHoldsOnPatronsPossessions', '0' );
447 $query->param( 'patron_id', $patron->{borrowernumber});
448 $query->param( 'bib_id', $item3->biblionumber);
449 $query->param( 'item_id', $item3->itemnumber);
450 $query->param( 'pickup_location', $origin_branch->{branchcode});
451 $reply = C4::ILSDI::Services::HoldItem( $query );
453 is( $reply->{code}, 'alreadypossession', "Patron has issued same book" );
454 is( $reply->{pickup_location}, undef, "No reserve placed");
456 # Test Patron cannot reserve if expired and BlockExpiredPatronOpacActions
457 my $category = $builder->build({
458 source => 'Category',
459 value => { BlockExpiredPatronOpacActions => -1 }
462 my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
464 my $expired_borrowernumber = Koha::Patron->new({
465 firstname => 'Expired',
467 categorycode => $category->{categorycode},
468 branchcode => $branch_1,
469 dateexpiry => '2000-01-01',
470 })->store->borrowernumber;
472 t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
474 my $item5 = $builder->build({
477 biblionumber => $biblio_with_no_item->biblionumber,
483 $query->param( 'patron_id', $expired_borrowernumber);
484 $query->param( 'bib_id', $biblio_with_no_item->biblionumber);
485 $query->param( 'item_id', $item5->{itemnumber});
487 $reply = C4::ILSDI::Services::HoldItem( $query );
488 is( $reply->{code}, 'PatronExpired', "Patron is expired" );
490 $schema->storage->txn_rollback;
493 subtest 'Holds test for branch transfer limits' => sub {
497 $schema->storage->txn_begin;
499 # Test enforement of branch transfer limits
500 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits', '1' );
501 t::lib::Mocks::mock_preference( 'BranchTransferLimitsType', 'itemtype' );
503 my $patron = $builder->build({
504 source => 'Borrower',
507 my $origin_branch = $builder->build(
511 pickup_location => 1,
515 my $pickup_branch = $builder->build(
519 pickup_location => 1,
524 my $item = $builder->build_sample_item(
526 library => $origin_branch->{branchcode},
530 Koha::CirculationRules->set_rule(
532 categorycode => undef,
535 rule_name => 'reservesallowed',
540 my $limit = Koha::Item::Transfer::Limit->new({
541 toBranch => $pickup_branch->{branchcode},
542 fromBranch => $item->holdingbranch,
543 itemtype => $item->effective_itemtype,
546 my $query = CGI->new;
547 $query->param( 'pickup_location', $pickup_branch->{branchcode} );
548 $query->param( 'patron_id', $patron->{borrowernumber});
549 $query->param( 'bib_id', $item->biblionumber);
550 $query->param( 'item_id', $item->itemnumber);
552 my $reply = C4::ILSDI::Services::HoldItem( $query );
553 is( $reply->{code}, 'cannotBeTransferred', "Item hold, Item cannot be transferred" );
555 $reply = C4::ILSDI::Services::HoldTitle( $query );
556 is( $reply->{code}, 'cannotBeTransferred', "Record hold, Item cannot be transferred" );
558 t::lib::Mocks::mock_preference( 'UseBranchTransferLimits', '0' );
560 $reply = C4::ILSDI::Services::HoldItem( $query );
561 is( $reply->{code}, undef, "Item hold, Item can be transferred" );
562 my $hold = Koha::Holds->search({ itemnumber => $item->itemnumber, borrowernumber => $patron->{borrowernumber} })->next;
563 is( $hold->branchcode, $pickup_branch->{branchcode}, 'The library id is correctly set' );
565 Koha::Holds->search()->delete();
567 $reply = C4::ILSDI::Services::HoldTitle( $query );
568 is( $reply->{code}, undef, "Record hold, Item con be transferred" );
569 $hold = Koha::Holds->search({ biblionumber => $item->biblionumber, borrowernumber => $patron->{borrowernumber} })->next;
570 is( $hold->branchcode, $pickup_branch->{branchcode}, 'The library id is correctly set' );
572 $schema->storage->txn_rollback;
575 subtest 'Holds test with start_date and end_date' => sub {
579 $schema->storage->txn_begin;
581 my $pickup_library = $builder->build_object(
583 class => 'Koha::Libraries',
585 pickup_location => 1,
590 my $patron = $builder->build_object({
591 class => 'Koha::Patrons',
594 my $item = $builder->build_sample_item({ library => $pickup_library->branchcode });
596 Koha::CirculationRules->set_rule(
598 categorycode => undef,
601 rule_name => 'reservesallowed',
606 my $query = CGI->new;
607 $query->param( 'pickup_location', $pickup_library->branchcode );
608 $query->param( 'patron_id', $patron->borrowernumber);
609 $query->param( 'bib_id', $item->biblionumber);
610 $query->param( 'item_id', $item->itemnumber);
611 $query->param( 'start_date', '2020-03-20');
612 $query->param( 'expiry_date', '2020-04-22');
614 my $reply = C4::ILSDI::Services::HoldItem( $query );
615 is ($reply->{pickup_location}, $pickup_library->branchname, "Item hold with date parameters was placed");
616 my $hold = Koha::Holds->search({ biblionumber => $item->biblionumber})->next();
617 is( $hold->biblionumber, $item->biblionumber, "correct biblionumber");
618 is( $hold->reservedate, '2020-03-20', "Item hold has correct start date" );
619 is( $hold->expirationdate, '2020-04-22', "Item hold has correct end date" );
623 $reply = C4::ILSDI::Services::HoldTitle( $query );
624 is ($reply->{pickup_location}, $pickup_library->branchname, "Record hold with date parameters was placed");
625 $hold = Koha::Holds->search({ biblionumber => $item->biblionumber})->next();
626 is( $hold->biblionumber, $item->biblionumber, "correct biblionumber");
627 is( $hold->reservedate, '2020-03-20', "Record hold has correct start date" );
628 is( $hold->expirationdate, '2020-04-22', "Record hold has correct end date" );
630 $schema->storage->txn_rollback;
633 subtest 'GetRecords' => sub {
637 $schema->storage->txn_begin;
639 t::lib::Mocks::mock_preference( 'ILS-DI', 1 );
641 my $branch1 = $builder->build({
644 my $branch2 = $builder->build({
648 my $item = $builder->build_sample_item(
650 library => $branch1->{branchcode},
654 my $patron = $builder->build({
655 source => 'Borrower',
658 my $issue = $builder->build({
661 itemnumber => $item->itemnumber,
665 my $hold = $builder->build({
668 biblionumber => $item->biblionumber,
672 ModItemTransfer($item->itemnumber, $branch1->{branchcode}, $branch2->{branchcode}, 'Manual');
675 $cgi->param(service => 'GetRecords');
676 $cgi->param(id => $item->biblionumber);
678 my $reply = C4::ILSDI::Services::GetRecords($cgi);
680 my $transfer = $item->get_transfer;
682 datesent => $transfer->datesent,
683 frombranch => $transfer->frombranch,
684 tobranch => $transfer->tobranch,
686 is_deeply($reply->{record}->[0]->{items}->{item}->[0]->{transfer}, $expected,
687 'GetRecords returns transfer informations');
689 # Check informations exposed
690 my $reply_issue = $reply->{record}->[0]->{issues}->{issue}->[0];
691 is($reply_issue->{itemnumber}, $item->itemnumber, 'GetRecords has an issue tag');
692 is($reply_issue->{borrowernumber}, undef, 'GetRecords does not expose borrowernumber in issue tag');
693 is($reply_issue->{surname}, undef, 'GetRecords does not expose surname in issue tag');
694 is($reply_issue->{firstname}, undef, 'GetRecords does not expose firstname in issue tag');
695 is($reply_issue->{cardnumber}, undef, 'GetRecords does not expose cardnumber in issue tag');
696 my $reply_reserve = $reply->{record}->[0]->{reserves}->{reserve}->[0];
697 is($reply_reserve->{biblionumber}, $item->biblionumber, 'GetRecords has a reserve tag');
698 is($reply_reserve->{borrowernumber}, undef, 'GetRecords does not expose borrowernumber in reserve tag');
700 $schema->storage->txn_rollback;
703 subtest 'RenewHold' => sub {
706 $schema->storage->txn_begin;
709 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
710 my $item = $builder->build_sample_item;
711 $cgi->param( patron_id => $patron->borrowernumber );
712 $cgi->param( item_id => $item->itemnumber );
714 t::lib::Mocks::mock_userenv( { patron => $patron } ); # For AddIssue
715 my $checkout = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
718 my $reply = C4::ILSDI::Services::RenewLoan($cgi);
719 is( exists $reply->{date_due}, 1, 'If the item is checked out, the date_due key should exist' );
721 # The item is not checked out
723 $reply = C4::ILSDI::Services::RenewLoan($cgi);
724 is( $reply, undef, 'If the item is not checked out, we should not explode.'); # FIXME We should return an error code instead
726 # The item does not exist
728 $reply = C4::ILSDI::Services::RenewLoan($cgi);
729 is( $reply->{code}, 'RecordNotFound', 'If the item does not exist, RecordNotFound should be returned');
732 $reply = C4::ILSDI::Services::RenewLoan($cgi);
733 is( $reply->{code}, 'PatronNotFound', 'If the patron does not exist, PatronNotFound should be returned');
735 $schema->storage->txn_rollback;
738 subtest 'GetPatronInfo paginated loans' => sub {
741 $schema->storage->txn_begin;
743 my $library = $builder->build_object({
744 class => 'Koha::Libraries',
747 my $item1 = $builder->build_sample_item({ library => $library->branchcode });
748 my $item2 = $builder->build_sample_item({ library => $library->branchcode });
749 my $item3 = $builder->build_sample_item({ library => $library->branchcode });
750 my $patron = $builder->build_object({
751 class => 'Koha::Patrons',
753 branchcode => $library->branchcode,
756 my $module = Test::MockModule->new('C4::Context');
757 $module->mock('userenv', sub { { branch => $library->branchcode } });
758 my $date_due = Koha::DateUtils::dt_from_string()->add(weeks => 2);
759 my $issue1 = C4::Circulation::AddIssue($patron->unblessed, $item1->barcode, $date_due);
760 my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due );
761 my $issue2 = C4::Circulation::AddIssue($patron->unblessed, $item2->barcode, $date_due);
762 my $date_due2 = Koha::DateUtils::dt_from_string( $issue2->date_due );
763 my $issue3 = C4::Circulation::AddIssue($patron->unblessed, $item3->barcode, $date_due);
764 my $date_due3 = Koha::DateUtils::dt_from_string( $issue3->date_due );
768 $cgi->param( 'service', 'GetPatronInfo' );
769 $cgi->param( 'patron_id', $patron->borrowernumber );
770 $cgi->param( 'show_loans', '1' );
771 $cgi->param( 'loans_per_page', '2' );
772 $cgi->param( 'loans_page', '1' );
773 my $reply = C4::ILSDI::Services::GetPatronInfo($cgi);
775 is($reply->{total_loans}, 3, 'total_loans == 3');
776 is(scalar @{ $reply->{loans}->{loan} }, 2, 'GetPatronInfo returned only 2 loans');
777 is($reply->{loans}->{loan}->[0]->{itemnumber}, $item3->itemnumber);
778 is($reply->{loans}->{loan}->[1]->{itemnumber}, $item2->itemnumber);
780 $cgi->param( 'loans_page', '2' );
781 $reply = C4::ILSDI::Services::GetPatronInfo($cgi);
783 is($reply->{total_loans}, 3, 'total_loans == 3');
784 is(scalar @{ $reply->{loans}->{loan} }, 1, 'GetPatronInfo returned only 1 loan');
785 is($reply->{loans}->{loan}->[0]->{itemnumber}, $item1->itemnumber);
787 $schema->storage->txn_rollback;