Bug 22379: Unit tests for CancelHold service
[koha.git] / t / db_dependent / ILSDI_Services.t
1 #!/usr/bin/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 CGI qw ( -utf8 );
21
22 use Test::More tests => 12;
23 use Test::MockModule;
24 use t::lib::Mocks;
25 use t::lib::TestBuilder;
26 use t::lib::Dates;
27 use XML::LibXML;
28
29 use C4::Items qw( ModItemTransfer );
30 use C4::Circulation qw( AddIssue );
31 use C4::Reserves qw (AddReserve ModReserve ModReserveAffect ModReserveStatus);
32
33 use Koha::AuthUtils;
34 use Koha::DateUtils qw( dt_from_string );
35
36 BEGIN {
37     use_ok('C4::ILSDI::Services', qw( AuthenticatePatron GetPatronInfo LookupPatron HoldTitle HoldItem GetRecords RenewLoan GetAvailability ));
38 }
39
40 my $schema  = Koha::Database->schema;
41 my $dbh     = C4::Context->dbh;
42 my $builder = t::lib::TestBuilder->new;
43
44 subtest 'AuthenticatePatron test' => sub {
45
46     plan tests => 16;
47
48     $schema->storage->txn_begin;
49
50     my $plain_password = 'tomasito';
51
52     $builder->build({
53         source => 'Borrower',
54         value => {
55             cardnumber => undef,
56         }
57     });
58
59     my $borrower = $builder->build({
60         source => 'Borrower',
61         value  => {
62             cardnumber => undef,
63             password => Koha::AuthUtils::hash_password( $plain_password ),
64             lastseen => "2001-01-01 12:34:56"
65         }
66     });
67
68     my $query = CGI->new;
69     $query->param( 'username', $borrower->{userid});
70     $query->param( 'password', $plain_password);
71
72     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '' );
73     my $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
74     is( $reply->{id}, $borrower->{borrowernumber}, "userid and password - Patron authenticated" );
75     is( $reply->{code}, undef, "Error code undef");
76     my $seen_patron = Koha::Patrons->find({ borrowernumber => $reply->{id} });
77     is( $seen_patron->lastseen(), '2001-01-01 12:34:56','Last seen not updated if not tracking patrons');
78
79     $query->param('password','ilsdi-passworD');
80     $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
81     is( $reply->{code}, 'PatronNotFound', "userid and wrong password - PatronNotFound" );
82     is( $reply->{id}, undef, "id undef");
83
84     $query->param( 'password', $plain_password );
85     $query->param( 'username', 'wrong-ilsdi-useriD' );
86     $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
87     is( $reply->{code}, 'PatronNotFound', "non-existing userid - PatronNotFound" );
88     is( $reply->{id}, undef, "id undef");
89
90     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
91     $query->param( 'username', uc( $borrower->{userid} ));
92     $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
93     my $now = dt_from_string;
94     is( $reply->{id}, $borrower->{borrowernumber}, "userid is not case sensitive - Patron authenticated" );
95     is( $reply->{code}, undef, "Error code undef");
96     $seen_patron = Koha::Patrons->find({ borrowernumber => $reply->{id} });
97     is( t::lib::Dates::compare( $seen_patron->lastseen, $now), 0, 'Last seen updated to today if tracking patrons' );
98
99     $query->param( 'username', $borrower->{cardnumber} );
100     $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
101     is( $reply->{id}, $borrower->{borrowernumber}, "cardnumber and password - Patron authenticated" );
102     is( $reply->{code}, undef, "Error code undef" );
103
104     $query->param( 'password', 'ilsdi-passworD' );
105     $reply = C4::ILSDI::Services::AuthenticatePatron( $query );
106     is( $reply->{code}, 'PatronNotFound', "cardnumber and wrong password - PatronNotFount" );
107     is( $reply->{id}, undef, "id undef" );
108
109     $query->param( 'username', 'randomcardnumber1234' );
110     $query->param( 'password', $plain_password );
111     $reply = C4::ILSDI::Services::AuthenticatePatron($query);
112     is( $reply->{code}, 'PatronNotFound', "non-existing cardnumer/userid - PatronNotFound" );
113     is( $reply->{id}, undef, "id undef");
114
115     $schema->storage->txn_rollback;
116 };
117
118
119 subtest 'GetPatronInfo/GetBorrowerAttributes test for extended patron attributes' => sub {
120
121     plan tests => 5;
122
123     $schema->storage->txn_begin;
124
125     $schema->resultset( 'Issue' )->delete_all;
126     $schema->resultset( 'Borrower' )->delete_all;
127     $schema->resultset( 'BorrowerAttribute' )->delete_all;
128     $schema->resultset( 'BorrowerAttributeType' )->delete_all;
129     $schema->resultset( 'Category' )->delete_all;
130     $schema->resultset( 'Item' )->delete_all; # 'Branch' deps. on this
131     $schema->resultset( 'Club' )->delete_all;
132     $schema->resultset( 'Branch' )->delete_all;
133
134     # Configure Koha to enable ILS-DI server and extended attributes:
135     t::lib::Mocks::mock_preference( 'ILS-DI', 1 );
136     t::lib::Mocks::mock_preference( 'ExtendedPatronAttributes', 1 );
137
138     # Set up a library/branch for our user to belong to:
139     my $lib = $builder->build( {
140         source => 'Branch',
141         value => {
142             branchcode => 'T_ILSDI',
143         }
144     } );
145
146     # Create a new category for user to belong to:
147     my $cat = $builder->build( {
148         source => 'Category',
149         value  => {
150             category_type                 => 'A',
151             BlockExpiredPatronOpacActions => -1,
152         }
153     } );
154
155     # Create a new attribute type:
156     my $attr_type = $builder->build( {
157         source => 'BorrowerAttributeType',
158         value  => {
159             code                      => 'HIDEME',
160             opac_display              => 0,
161             authorised_value_category => '',
162             class                     => '',
163         }
164     } );
165     my $attr_type_visible = $builder->build( {
166         source => 'BorrowerAttributeType',
167         value  => {
168             code                      => 'SHOWME',
169             opac_display              => 1,
170             authorised_value_category => '',
171             class                     => '',
172         }
173     } );
174
175     # Create a new user:
176     my $brwr = $builder->build( {
177         source => 'Borrower',
178         value  => {
179             categorycode => $cat->{'categorycode'},
180             branchcode   => $lib->{'branchcode'},
181         }
182     } );
183
184     # Authorised value:
185     my $auth = $builder->build( {
186         source => 'AuthorisedValue',
187         value  => {
188             category => $cat->{'categorycode'}
189         }
190     } );
191
192     # Set the new attribute for our user:
193     my $attr_hidden = $builder->build( {
194         source => 'BorrowerAttribute',
195         value  => {
196             borrowernumber => $brwr->{'borrowernumber'},
197             code           => $attr_type->{'code'},
198             attribute      => '1337 hidden',
199         }
200     } );
201     my $attr_shown = $builder->build( {
202         source => 'BorrowerAttribute',
203         value  => {
204             borrowernumber => $brwr->{'borrowernumber'},
205             code           => $attr_type_visible->{'code'},
206             attribute      => '1337 shown',
207         }
208     } );
209
210     my $fine = $builder->build(
211         {
212             source => 'Accountline',
213             value  => {
214                 borrowernumber    => $brwr->{borrowernumber},
215                 debit_type_code   => 'OVERDUE',
216                 amountoutstanding => 10
217             }
218         }
219     );
220
221     # Prepare and send web request for IL-SDI server:
222     my $query = CGI->new;
223     $query->param( 'service', 'GetPatronInfo' );
224     $query->param( 'patron_id', $brwr->{'borrowernumber'} );
225     $query->param( 'show_attributes', '1' );
226     $query->param( 'show_fines', '1' );
227
228     my $reply = C4::ILSDI::Services::GetPatronInfo( $query );
229
230     # Build a structure for comparison:
231     my $cmp = {
232         borrowernumber    => $brwr->{borrowernumber},
233         value             => $attr_shown->{'attribute'},
234         value_description => $attr_shown->{'attribute'},
235         %$attr_type_visible,
236         %$attr_shown,
237     };
238
239     is( $reply->{'charges'}, '10.00',
240         'The \'charges\' attribute should be correctly filled (bug 17836)' );
241
242     is( scalar( @{$reply->{fines}->{fine}}), 1, 'There should be only 1 account line');
243     is(
244         $reply->{fines}->{fine}->[0]->{accountlines_id},
245         $fine->{accountlines_id},
246         "The accountline should be the correct one"
247     );
248
249     # Check results:
250     is_deeply( $reply->{'attributes'}, [ $cmp ], 'Test GetPatronInfo - show_attributes parameter' );
251
252     ok( exists $reply->{is_expired}, 'There should be the is_expired information');
253
254     # Cleanup
255     $schema->storage->txn_rollback;
256 };
257
258 subtest 'LookupPatron test' => sub {
259
260     plan tests => 9;
261
262     $schema->storage->txn_begin;
263
264     $schema->resultset( 'Issue' )->delete_all;
265     $schema->resultset( 'Borrower' )->delete_all;
266     $schema->resultset( 'BorrowerAttribute' )->delete_all;
267     $schema->resultset( 'BorrowerAttributeType' )->delete_all;
268     $schema->resultset( 'Category' )->delete_all;
269     $schema->resultset( 'Item' )->delete_all; # 'Branch' deps. on this
270     $schema->resultset( 'Branch' )->delete_all;
271
272     my $borrower = $builder->build({
273         source => 'Borrower',
274     });
275
276     my $query = CGI->new();
277     my $bad_result = C4::ILSDI::Services::LookupPatron($query);
278     is( $bad_result->{message}, 'PatronNotFound', 'No parameters' );
279
280     $query->delete_all();
281     $query->param( 'id', $borrower->{firstname} );
282     my $optional_result = C4::ILSDI::Services::LookupPatron($query);
283     is(
284         $optional_result->{id},
285         $borrower->{borrowernumber},
286         'Valid Firstname only'
287     );
288
289     $query->delete_all();
290     $query->param( 'id', 'ThereIsNoWayThatThisCouldPossiblyBeValid' );
291     my $bad_optional_result = C4::ILSDI::Services::LookupPatron($query);
292     is( $bad_optional_result->{message}, 'PatronNotFound', 'Invalid ID' );
293
294     foreach my $id_type (
295         'cardnumber',
296         'userid',
297         'email',
298         'borrowernumber',
299         'surname',
300         'firstname'
301     ) {
302         $query->delete_all();
303         $query->param( 'id_type', $id_type );
304         $query->param( 'id', $borrower->{$id_type} );
305         my $result = C4::ILSDI::Services::LookupPatron($query);
306         is( $result->{'id'}, $borrower->{borrowernumber}, "Checking $id_type" );
307     }
308
309     # Cleanup
310     $schema->storage->txn_rollback;
311 };
312
313 subtest 'Holds test' => sub {
314
315     plan tests => 9;
316
317     $schema->storage->txn_begin;
318
319     t::lib::Mocks::mock_preference( 'AllowHoldsOnDamagedItems', 0 );
320
321     my $patron = $builder->build({
322         source => 'Borrower',
323     });
324
325     my $item = $builder->build_sample_item(
326         {
327             damaged => 1
328         }
329     );
330
331     my $query = CGI->new;
332     $query->param( 'patron_id', $patron->{borrowernumber});
333     $query->param( 'bib_id', $item->biblionumber);
334
335     my $reply = C4::ILSDI::Services::HoldTitle( $query );
336     is( $reply->{code}, 'damaged', "Item damaged" );
337
338     $item->damaged(0)->store;
339
340     my $hold = $builder->build({
341         source => 'Reserve',
342         value => {
343             borrowernumber => $patron->{borrowernumber},
344             biblionumber => $item->biblionumber,
345             itemnumber => $item->itemnumber
346         }
347     });
348
349     $reply = C4::ILSDI::Services::HoldTitle( $query );
350     is( $reply->{code}, 'itemAlreadyOnHold', "Item already on hold" );
351
352     my $biblio_with_no_item = $builder->build_sample_biblio;
353
354     $query = CGI->new;
355     $query->param( 'patron_id', $patron->{borrowernumber});
356     $query->param( 'bib_id', $biblio_with_no_item->biblionumber);
357
358     $reply = C4::ILSDI::Services::HoldTitle( $query );
359     is( $reply->{code}, 'NoItems', 'Biblio has no item' );
360
361     my $item2 = $builder->build_sample_item(
362         {
363             damaged => 0,
364         }
365     );
366
367     t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
368     Koha::CirculationRules->set_rule(
369         {
370             categorycode => $patron->{categorycode},
371             itemtype     => $item2->{itype},
372             branchcode   => $patron->{branchcode},
373             rule_name    => 'reservesallowed',
374             rule_value   => 1,
375         }
376     );
377
378     $query = CGI->new;
379     $query->param( 'patron_id', $patron->{borrowernumber});
380     $query->param( 'bib_id', $item2->biblionumber);
381     $query->param( 'item_id', $item2->itemnumber);
382
383     $reply = C4::ILSDI::Services::HoldItem( $query );
384     is( $reply->{code}, 'tooManyReserves', "Too many reserves" );
385
386     Koha::CirculationRules->set_rule(
387         {
388             categorycode => $patron->{categorycode},
389             itemtype     => $item2->{itype},
390             branchcode   => $patron->{branchcode},
391             rule_name    => 'reservesallowed',
392             rule_value   => 0,
393         }
394     );
395
396     $query = CGI->new;
397     $query->param( 'patron_id', $patron->{borrowernumber});
398     $query->param( 'bib_id', $item2->biblionumber);
399     $query->param( 'item_id', $item2->itemnumber);
400
401     $reply = C4::ILSDI::Services::HoldItem( $query );
402     is( $reply->{code}, 'noReservesAllowed', "No reserves allowed" );
403
404     my $origin_branch = $builder->build(
405         {
406             source => 'Branch',
407             value  => {
408                 pickup_location => 1,
409             }
410         }
411     );
412
413     # Adding a holdable item.
414     my $item3 = $builder->build_sample_item(
415        {
416            barcode => '123456789',
417            library => $origin_branch->{branchcode}
418        });
419
420     my $item4 = $builder->build_sample_item(
421         {
422            biblionumber => $item3->biblionumber,
423            damaged => 1,
424            library => $origin_branch->{branchcode}
425        });
426
427     Koha::CirculationRules->set_rule(
428         {
429             categorycode => $patron->{categorycode},
430             itemtype     => $item3->{itype},
431             branchcode   => $patron->{branchcode},
432             rule_name    => 'reservesallowed',
433             rule_value   => 10,
434         }
435     );
436
437     $query = CGI->new;
438     $query->param( 'patron_id', $patron->{borrowernumber});
439     $query->param( 'bib_id', $item4->biblionumber);
440     $query->param( 'item_id', $item4->itemnumber);
441
442     $reply = C4::ILSDI::Services::HoldItem( $query );
443     is( $reply->{code}, 'damaged', "Item is damaged" );
444
445     my $module = Test::MockModule->new('C4::Context');
446     $module->mock('userenv', sub { { patron => $patron } });
447     my $issue = C4::Circulation::AddIssue($patron, $item3->barcode);
448     t::lib::Mocks::mock_preference( 'AllowHoldsOnPatronsPossessions', '0' );
449
450     $query = CGI->new;
451     $query->param( 'patron_id', $patron->{borrowernumber});
452     $query->param( 'bib_id', $item3->biblionumber);
453     $query->param( 'item_id', $item3->itemnumber);
454     $query->param( 'pickup_location', $origin_branch->{branchcode});
455     $reply = C4::ILSDI::Services::HoldItem( $query );
456
457     is( $reply->{code}, 'alreadypossession', "Patron has issued same book" );
458     is( $reply->{pickup_location}, undef, "No reserve placed");
459
460     # Test Patron cannot reserve if expired and BlockExpiredPatronOpacActions
461     my $category = $builder->build({
462         source => 'Category',
463         value => { BlockExpiredPatronOpacActions => -1 }
464         });
465
466     my $branch_1 = $builder->build({ source => 'Branch' })->{ branchcode };
467
468     my $expired_borrowernumber = Koha::Patron->new({
469         firstname =>  'Expired',
470         surname => 'Patron',
471         categorycode => $category->{categorycode},
472         branchcode => $branch_1,
473         dateexpiry => '2000-01-01',
474     })->store->borrowernumber;
475
476     t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 1);
477
478     my $item5 = $builder->build({
479         source => 'Item',
480         value => {
481             biblionumber => $biblio_with_no_item->biblionumber,
482             damaged => 0,
483         }
484     });
485
486     $query = CGI->new;
487     $query->param( 'patron_id', $expired_borrowernumber);
488     $query->param( 'bib_id', $biblio_with_no_item->biblionumber);
489     $query->param( 'item_id', $item5->{itemnumber});
490
491     $reply = C4::ILSDI::Services::HoldItem( $query );
492     is( $reply->{code}, 'PatronExpired', "Patron is expired" );
493
494     $schema->storage->txn_rollback;
495 };
496
497 subtest 'Holds test for branch transfer limits' => sub {
498
499     plan tests => 6;
500
501     $schema->storage->txn_begin;
502
503     # Test enforement of branch transfer limits
504     t::lib::Mocks::mock_preference( 'UseBranchTransferLimits', '1' );
505     t::lib::Mocks::mock_preference( 'BranchTransferLimitsType', 'itemtype' );
506
507     my $patron = $builder->build({
508         source => 'Borrower',
509     });
510
511     my $origin_branch = $builder->build(
512         {
513             source => 'Branch',
514             value  => {
515                 pickup_location => 1,
516             }
517         }
518     );
519     my $pickup_branch = $builder->build(
520         {
521             source => 'Branch',
522             value  => {
523                 pickup_location => 1,
524             }
525         }
526     );
527
528     my $item = $builder->build_sample_item(
529         {
530             library => $origin_branch->{branchcode},
531         }
532     );
533
534     Koha::CirculationRules->set_rule(
535         {
536             categorycode => undef,
537             itemtype     => undef,
538             branchcode   => undef,
539             rule_name    => 'reservesallowed',
540             rule_value   => 99,
541         }
542     );
543
544     my $limit = Koha::Item::Transfer::Limit->new({
545         toBranch => $pickup_branch->{branchcode},
546         fromBranch => $item->holdingbranch,
547         itemtype => $item->effective_itemtype,
548     })->store();
549
550     my $query = CGI->new;
551     $query->param( 'pickup_location', $pickup_branch->{branchcode} );
552     $query->param( 'patron_id', $patron->{borrowernumber});
553     $query->param( 'bib_id', $item->biblionumber);
554     $query->param( 'item_id', $item->itemnumber);
555
556     my $reply = C4::ILSDI::Services::HoldItem( $query );
557     is( $reply->{code}, 'cannotBeTransferred', "Item hold, Item cannot be transferred" );
558
559     $reply = C4::ILSDI::Services::HoldTitle( $query );
560     is( $reply->{code}, 'cannotBeTransferred', "Record hold, Item cannot be transferred" );
561
562     t::lib::Mocks::mock_preference( 'UseBranchTransferLimits', '0' );
563
564     $reply = C4::ILSDI::Services::HoldItem( $query );
565     is( $reply->{code}, undef, "Item hold, Item can be transferred" );
566     my $hold = Koha::Holds->search({ itemnumber => $item->itemnumber, borrowernumber => $patron->{borrowernumber} })->next;
567     is( $hold->branchcode, $pickup_branch->{branchcode}, 'The library id is correctly set' );
568
569     Koha::Holds->search()->delete();
570
571     $reply = C4::ILSDI::Services::HoldTitle( $query );
572     is( $reply->{code}, undef, "Record hold, Item con be transferred" );
573     $hold = Koha::Holds->search({ biblionumber => $item->biblionumber, borrowernumber => $patron->{borrowernumber} })->next;
574     is( $hold->branchcode, $pickup_branch->{branchcode}, 'The library id is correctly set' );
575
576     $schema->storage->txn_rollback;
577 };
578
579 subtest 'Holds test with start_date and end_date' => sub {
580
581     plan tests => 8;
582
583     $schema->storage->txn_begin;
584
585     my $pickup_library = $builder->build_object(
586         {
587             class  => 'Koha::Libraries',
588             value  => {
589                 pickup_location => 1,
590             }
591         }
592     );
593
594     my $patron = $builder->build_object({
595         class => 'Koha::Patrons',
596     });
597
598     my $item = $builder->build_sample_item({ library => $pickup_library->branchcode });
599
600     Koha::CirculationRules->set_rule(
601         {
602             categorycode => undef,
603             itemtype     => undef,
604             branchcode   => undef,
605             rule_name    => 'reservesallowed',
606             rule_value   => 99,
607         }
608     );
609
610     my $query = CGI->new;
611     $query->param( 'pickup_location', $pickup_library->branchcode );
612     $query->param( 'patron_id', $patron->borrowernumber);
613     $query->param( 'bib_id', $item->biblionumber);
614     $query->param( 'item_id', $item->itemnumber);
615     $query->param( 'start_date', '2020-03-20');
616     $query->param( 'expiry_date', '2020-04-22');
617
618     my $reply = C4::ILSDI::Services::HoldItem( $query );
619     is ($reply->{pickup_location}, $pickup_library->branchname, "Item hold with date parameters was placed");
620     my $hold = Koha::Holds->search({ biblionumber => $item->biblionumber})->next();
621     is( $hold->biblionumber, $item->biblionumber, "correct biblionumber");
622     is( $hold->reservedate, '2020-03-20', "Item hold has correct start date" );
623     is( $hold->expirationdate, '2020-04-22', "Item hold has correct end date" );
624
625     $hold->delete();
626
627     $reply = C4::ILSDI::Services::HoldTitle( $query );
628     is ($reply->{pickup_location}, $pickup_library->branchname, "Record hold with date parameters was placed");
629     $hold = Koha::Holds->search({ biblionumber => $item->biblionumber})->next();
630     is( $hold->biblionumber, $item->biblionumber, "correct biblionumber");
631     is( $hold->reservedate, '2020-03-20', "Record hold has correct start date" );
632     is( $hold->expirationdate, '2020-04-22', "Record hold has correct end date" );
633
634     $schema->storage->txn_rollback;
635 };
636
637 subtest 'GetRecords' => sub {
638
639     plan tests => 8;
640
641     $schema->storage->txn_begin;
642
643     t::lib::Mocks::mock_preference( 'ILS-DI', 1 );
644
645     my $branch1 = $builder->build({
646         source => 'Branch',
647     });
648     my $branch2 = $builder->build({
649         source => 'Branch',
650     });
651
652     my $item = $builder->build_sample_item(
653         {
654             library => $branch1->{branchcode},
655         }
656     );
657
658     my $patron = $builder->build({
659         source => 'Borrower',
660     });
661
662     my $issue = $builder->build({
663         source => 'Issue',
664         value => {
665             itemnumber => $item->itemnumber,
666         }
667     });
668
669     my $hold = $builder->build({
670         source => 'Reserve',
671         value => {
672             biblionumber => $item->biblionumber,
673         }
674     });
675
676     ModItemTransfer($item->itemnumber, $branch1->{branchcode}, $branch2->{branchcode}, 'Manual');
677
678     my $cgi = CGI->new;
679     $cgi->param(service => 'GetRecords');
680     $cgi->param(id => $item->biblionumber);
681
682     my $reply = C4::ILSDI::Services::GetRecords($cgi);
683
684     my $transfer = $item->get_transfer;
685     my $expected = {
686         datesent => $transfer->datesent,
687         frombranch => $transfer->frombranch,
688         tobranch => $transfer->tobranch,
689     };
690     is_deeply($reply->{record}->[0]->{items}->{item}->[0]->{transfer}, $expected,
691         'GetRecords returns transfer informations');
692
693     # Check informations exposed
694     my $reply_issue = $reply->{record}->[0]->{issues}->{issue}->[0];
695     is($reply_issue->{itemnumber}, $item->itemnumber, 'GetRecords has an issue tag');
696     is($reply_issue->{borrowernumber}, undef, 'GetRecords does not expose borrowernumber in issue tag');
697     is($reply_issue->{surname}, undef, 'GetRecords does not expose surname in issue tag');
698     is($reply_issue->{firstname}, undef, 'GetRecords does not expose firstname in issue tag');
699     is($reply_issue->{cardnumber}, undef, 'GetRecords does not expose cardnumber in issue tag');
700     my $reply_reserve = $reply->{record}->[0]->{reserves}->{reserve}->[0];
701     is($reply_reserve->{biblionumber}, $item->biblionumber, 'GetRecords has a reserve tag');
702     is($reply_reserve->{borrowernumber}, undef, 'GetRecords does not expose borrowernumber in reserve tag');
703
704     $schema->storage->txn_rollback;
705 };
706
707 subtest 'RenewHold' => sub {
708     plan tests => 4;
709
710     $schema->storage->txn_begin;
711
712     my $cgi    = CGI->new;
713     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
714     my $item   = $builder->build_sample_item;
715     $cgi->param( patron_id => $patron->borrowernumber );
716     $cgi->param( item_id   => $item->itemnumber );
717
718     t::lib::Mocks::mock_userenv( { patron => $patron } );    # For AddIssue
719     my $checkout = C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
720
721     # Everything is ok
722     my $reply = C4::ILSDI::Services::RenewLoan($cgi);
723     is( exists $reply->{date_due}, 1, 'If the item is checked out, the date_due key should exist' );
724
725     # The item is not checked out
726     $checkout->delete;
727     $reply = C4::ILSDI::Services::RenewLoan($cgi);
728     is( $reply, undef, 'If the item is not checked out, we should not explode.');    # FIXME We should return an error code instead
729
730     # The item does not exist
731     $item->delete;
732     $reply = C4::ILSDI::Services::RenewLoan($cgi);
733     is( $reply->{code}, 'RecordNotFound', 'If the item does not exist, RecordNotFound should be returned');
734
735     $patron->delete;
736     $reply = C4::ILSDI::Services::RenewLoan($cgi);
737     is( $reply->{code}, 'PatronNotFound', 'If the patron does not exist, PatronNotFound should be returned');
738
739     $schema->storage->txn_rollback;
740 };
741
742 subtest 'CancelHold' => sub {
743     plan tests => 4;
744
745     $schema->storage->txn_begin;
746
747     my $cgi = CGI->new;
748
749     my $library = $builder->build_object({
750         class => 'Koha::Libraries',
751     });
752
753     my $patron = $builder->build_object( { class => 'Koha::Patrons',
754                                            value => {
755                                                branchcode => $library->branchcode,
756                                            },
757                                          } );
758
759     my $patron2 = $builder->build_object( { class => 'Koha::Patrons',
760                                            value => {
761                                                branchcode => $library->branchcode,
762                                            },
763                                          } );
764
765     my $item = $builder->build_sample_item({ library => $library->branchcode });
766
767     my $reserve = C4::Reserves::AddReserve({branchcode => $library->branchcode,
768                                             borrowernumber => $patron->borrowernumber,
769                                             biblionumber => $item->biblionumber });
770
771     # Affecting the reserve sets it to a waiting state
772     C4::Reserves::ModReserveAffect( $item->itemnumber,
773                                     $patron->borrowernumber,
774                                     undef,
775                                     $reserve,
776                                    );
777
778     $cgi->param( patron_id => $patron2->borrowernumber );
779     $cgi->param( item_id   => $reserve );
780
781
782     my $reply = C4::ILSDI::Services::CancelHold($cgi);
783     is( $reply->{code}, 'BorrowerCannotCancelHold', 'If the patron is wrong, BorrowerCannotCancelHold should be returned');
784
785     $cgi->param( patron_id => $patron->borrowernumber );
786     $reply = C4::ILSDI::Services::CancelHold($cgi);
787     is( $reply->{code}, 'BorrowerCannotCancelHold', 'If reserve in a Waiting state, patron cannot cancel');
788
789     C4::Reserves::ModReserveStatus( $item->itemnumber, 'T' );
790     $reply = C4::ILSDI::Services::CancelHold($cgi);
791     is( $reply->{code}, 'BorrowerCannotCancelHold', 'If reserve in a Transfer state, patron cannot cancel');
792
793     C4::Reserves::ModReserve( {rank => 1, reserve_id => $reserve, branchcode => $library->branchcode} );
794     $cgi->param( item_id => $reserve );
795     $reply = C4::ILSDI::Services::CancelHold($cgi);
796     is( $reply->{code}, 'Canceled', 'If the patron is fine and reserve not waiting, Canceled should be returned and reserve canceled');
797
798     $schema->storage->txn_rollback;
799 };
800
801 subtest 'GetPatronInfo paginated loans' => sub {
802     plan tests => 7;
803
804     $schema->storage->txn_begin;
805
806     my $library = $builder->build_object({
807         class => 'Koha::Libraries',
808     });
809
810     my $item1 = $builder->build_sample_item({ library => $library->branchcode });
811     my $item2 = $builder->build_sample_item({ library => $library->branchcode });
812     my $item3 = $builder->build_sample_item({ library => $library->branchcode });
813     my $patron = $builder->build_object({
814         class => 'Koha::Patrons',
815         value => {
816             branchcode => $library->branchcode,
817         },
818     });
819     my $module = Test::MockModule->new('C4::Context');
820     $module->mock('userenv', sub { { branch => $library->branchcode } });
821     my $date_due = Koha::DateUtils::dt_from_string()->add(weeks => 2);
822     my $issue1 = C4::Circulation::AddIssue($patron->unblessed, $item1->barcode, $date_due);
823     my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due );
824     my $issue2 = C4::Circulation::AddIssue($patron->unblessed, $item2->barcode, $date_due);
825     my $date_due2 = Koha::DateUtils::dt_from_string( $issue2->date_due );
826     my $issue3 = C4::Circulation::AddIssue($patron->unblessed, $item3->barcode, $date_due);
827     my $date_due3 = Koha::DateUtils::dt_from_string( $issue3->date_due );
828
829     my $cgi = CGI->new;
830
831     $cgi->param( 'service', 'GetPatronInfo' );
832     $cgi->param( 'patron_id', $patron->borrowernumber );
833     $cgi->param( 'show_loans', '1' );
834     $cgi->param( 'loans_per_page', '2' );
835     $cgi->param( 'loans_page', '1' );
836     my $reply = C4::ILSDI::Services::GetPatronInfo($cgi);
837
838     is($reply->{total_loans}, 3, 'total_loans == 3');
839     is(scalar @{ $reply->{loans}->{loan} }, 2, 'GetPatronInfo returned only 2 loans');
840     is($reply->{loans}->{loan}->[0]->{itemnumber}, $item3->itemnumber);
841     is($reply->{loans}->{loan}->[1]->{itemnumber}, $item2->itemnumber);
842
843     $cgi->param( 'loans_page', '2' );
844     $reply = C4::ILSDI::Services::GetPatronInfo($cgi);
845
846     is($reply->{total_loans}, 3, 'total_loans == 3');
847     is(scalar @{ $reply->{loans}->{loan} }, 1, 'GetPatronInfo returned only 1 loan');
848     is($reply->{loans}->{loan}->[0]->{itemnumber}, $item1->itemnumber);
849
850     $schema->storage->txn_rollback;
851 };
852
853 subtest 'GetAvailability itemcallnumber' => sub {
854
855     plan tests => 4;
856
857     $schema->storage->txn_begin;
858
859     t::lib::Mocks::mock_preference( 'ILS-DI', 1 );
860
861     my $item1 = $builder->build_sample_item(
862         {
863             itemcallnumber => "callnumber",
864         }
865     );
866
867     my $item2 = $builder->build_sample_item( {} );
868
869     # Build the query
870     my $cgi = CGI->new;
871     $cgi->param( service => 'GetAvailability' );
872     $cgi->param( id      => $item1->itemnumber );
873     $cgi->param( id_type => 'item' );
874
875     # Output of GetAvailability is a string containing XML
876     my $reply = C4::ILSDI::Services::GetAvailability($cgi);
877
878     # Parse the output and get info
879     my $result_XML = XML::LibXML->load_xml( string => $reply );
880     my $reply_callnumber =
881       $result_XML->findnodes('//dlf:itemcallnumber')->to_literal();
882
883     # Test the output
884     is( $reply_callnumber, $item1->itemcallnumber,
885         "GetAvailability item has an itemcallnumber tag" );
886
887     $cgi = CGI->new;
888     $cgi->param( service => 'GetAvailability' );
889     $cgi->param( id      => $item2->itemnumber );
890     $cgi->param( id_type => 'item' );
891     $reply      = C4::ILSDI::Services::GetAvailability($cgi);
892     $result_XML = XML::LibXML->load_xml( string => $reply );
893     $reply_callnumber =
894       $result_XML->findnodes('//dlf:itemcallnumber')->to_literal();
895     is( $reply_callnumber, '',
896         "As expected, GetAvailability item has no itemcallnumber tag" );
897
898     $cgi = CGI->new;
899     $cgi->param( service => 'GetAvailability' );
900     $cgi->param( id      => $item1->biblionumber );
901     $cgi->param( id_type => 'biblio' );
902     $reply      = C4::ILSDI::Services::GetAvailability($cgi);
903     $result_XML = XML::LibXML->load_xml( string => $reply );
904     $reply_callnumber =
905       $result_XML->findnodes('//dlf:itemcallnumber')->to_literal();
906     is( $reply_callnumber, $item1->itemcallnumber,
907         "GetAvailability biblio has an itemcallnumber tag" );
908
909     $cgi = CGI->new;
910     $cgi->param( service => 'GetAvailability' );
911     $cgi->param( id      => $item2->biblionumber );
912     $cgi->param( id_type => 'biblio' );
913     $reply      = C4::ILSDI::Services::GetAvailability($cgi);
914     $result_XML = XML::LibXML->load_xml( string => $reply );
915     $reply_callnumber =
916       $result_XML->findnodes('//dlf:itemcallnumber')->to_literal();
917     is( $reply_callnumber, '',
918         "As expected, GetAvailability biblio has no itemcallnumber tag" );
919
920     $schema->storage->txn_rollback;
921 };