Bug 24606: Regression tests
[koha.git] / t / db_dependent / Koha / Hold.t
1 #!/usr/bin/perl
2
3 # Copyright 2020 Koha Development team
4 #
5 # This file is part of Koha
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 11;
23
24 use Test::Exception;
25 use Test::MockModule;
26
27 use t::lib::Mocks;
28 use t::lib::TestBuilder;
29 use t::lib::Mocks;
30
31 use Koha::ActionLogs;
32 use Koha::DateUtils qw(dt_from_string);
33 use Koha::Holds;
34 use Koha::Libraries;
35 use Koha::Old::Holds;
36
37 my $schema  = Koha::Database->new->schema;
38 my $builder = t::lib::TestBuilder->new;
39
40 subtest 'store() tests' => sub {
41     plan tests => 2;
42
43     $schema->storage->txn_begin;
44
45     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
46     my $item   = $builder->build_sample_item;
47     throws_ok {
48         Koha::Hold->new(
49             {
50                 borrowernumber => $patron->borrowernumber,
51                 biblionumber   => $item->biblionumber,
52                 priority       => 1,
53                 itemnumber     => $item->itemnumber,
54             }
55         )->store
56     }
57     'Koha::Exceptions::Hold::MissingPickupLocation',
58       'Exception thrown because branchcode was not passed';
59
60     my $hold = $builder->build_object( { class => 'Koha::Holds' } );
61     throws_ok {
62         $hold->branchcode(undef)->store;
63     }
64     'Koha::Exceptions::Hold::MissingPickupLocation',
65       'Exception thrown if one tries to set branchcode to null';
66
67     $schema->storage->txn_rollback;
68 };
69
70 subtest 'fill() tests' => sub {
71
72     plan tests => 13;
73
74     $schema->storage->txn_begin;
75
76     my $fee = 15;
77
78     my $category = $builder->build_object(
79         {
80             class => 'Koha::Patron::Categories',
81             value => { reservefee => $fee }
82         }
83     );
84     my $patron = $builder->build_object(
85         {
86             class => 'Koha::Patrons',
87             value => { categorycode => $category->id }
88         }
89     );
90     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
91
92     my $title  = 'Do what you want';
93     my $biblio = $builder->build_sample_biblio( { title => $title } );
94     my $item   = $builder->build_sample_item( { biblionumber => $biblio->id } );
95     my $hold   = $builder->build_object(
96         {
97             class => 'Koha::Holds',
98             value => {
99                 biblionumber   => $biblio->id,
100                 borrowernumber => $patron->id,
101                 itemnumber     => $item->id,
102                 priority       => 10,
103             }
104         }
105     );
106
107     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' );
108     t::lib::Mocks::mock_preference( 'HoldsLog',    1 );
109     t::lib::Mocks::mock_userenv(
110         { patron => $manager, branchcode => $manager->branchcode } );
111
112     my $interface = 'api';
113     C4::Context->interface($interface);
114
115     my $ret = $hold->fill;
116
117     is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
118     is( $ret->id, $hold->id, '->fill returns the object' );
119
120     is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
121     my $old_hold = Koha::Old::Holds->find( $hold->id );
122
123     is( $old_hold->id, $hold->id, 'reserve_id retained' );
124     is( $old_hold->priority, 0, 'priority set to 0' );
125     is( $old_hold->found, 'F', 'found set to F' );
126
127     subtest 'fee applied tests' => sub {
128
129         plan tests => 9;
130
131         my $account = $patron->account;
132         is( $account->balance, $fee, 'Charge applied correctly' );
133
134         my $debits = $account->outstanding_debits;
135         is( $debits->count, 1, 'Only one fee charged' );
136
137         my $fee_debit = $debits->next;
138         is( $fee_debit->amount * 1, $fee, 'Fee amount stored correctly' );
139         is( $fee_debit->description, $title,
140             'Fee description stored correctly' );
141         is( $fee_debit->manager_id, $manager->id,
142             'Fee manager_id stored correctly' );
143         is( $fee_debit->branchcode, $manager->branchcode,
144             'Fee branchcode stored correctly' );
145         is( $fee_debit->interface, $interface,
146             'Fee interface stored correctly' );
147         is( $fee_debit->debit_type_code,
148             'RESERVE', 'Fee debit_type_code stored correctly' );
149         is( $fee_debit->itemnumber, $item->id,
150             'Fee itemnumber stored correctly' );
151     };
152
153     my $logs = Koha::ActionLogs->search(
154         {
155             action => 'FILL',
156             module => 'HOLDS',
157             object => $hold->id
158         }
159     );
160
161     is( $logs->count, 1, '1 log line added' );
162
163     # Set HoldFeeMode to something other than any_time_is_collected
164     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
165     # Disable logging
166     t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
167
168     $hold = $builder->build_object(
169         {
170             class => 'Koha::Holds',
171             value => {
172                 biblionumber   => $biblio->id,
173                 borrowernumber => $patron->id,
174                 itemnumber     => $item->id,
175                 priority       => 10,
176             }
177         }
178     );
179
180     $hold->fill;
181
182     my $account = $patron->account;
183     is( $account->balance, $fee, 'No new charge applied' );
184
185     my $debits = $account->outstanding_debits;
186     is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
187
188     $logs = Koha::ActionLogs->search(
189         {
190             action => 'FILL',
191             module => 'HOLDS',
192             object => $hold->id
193         }
194     );
195
196     is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
197
198     subtest 'anonymization behavior tests' => sub {
199
200         plan tests => 5;
201
202         # reduce the tests noise
203         t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
204         t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
205         # unset AnonymousPatron
206         t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
207
208         # 0 == keep forever
209         $patron->privacy(0)->store;
210         my $hold = $builder->build_object(
211             {
212                 class => 'Koha::Holds',
213                 value => { borrowernumber => $patron->id, found => undef }
214             }
215         );
216         $hold->fill();
217         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
218             $patron->borrowernumber, 'Patron link is kept' );
219
220         # 1 == "default", meaning it is not protected from removal
221         $patron->privacy(1)->store;
222         $hold = $builder->build_object(
223             {
224                 class => 'Koha::Holds',
225                 value => { borrowernumber => $patron->id, found => undef }
226             }
227         );
228         $hold->fill();
229         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
230             $patron->borrowernumber, 'Patron link is kept' );
231
232         # 2 == delete immediately
233         $patron->privacy(2)->store;
234         $hold = $builder->build_object(
235             {
236                 class => 'Koha::Holds',
237                 value => { borrowernumber => $patron->id, found => undef }
238             }
239         );
240
241         throws_ok
242             { $hold->fill(); }
243             'Koha::Exception',
244             'AnonymousPatron not set, exception thrown';
245
246         $hold->discard_changes; # refresh from DB
247
248         ok( !$hold->is_found, 'Hold is not filled' );
249
250         my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
251         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
252
253         $hold = $builder->build_object(
254             {
255                 class => 'Koha::Holds',
256                 value => { borrowernumber => $patron->id, found => undef }
257             }
258         );
259         $hold->fill();
260         is(
261             Koha::Old::Holds->find( $hold->id )->borrowernumber,
262             $anonymous_patron->id,
263             'Patron link is set to the configured anonymous patron immediately'
264         );
265     };
266
267     subtest 'holds_queue update tests' => sub {
268
269         plan tests => 1;
270
271         my $biblio = $builder->build_sample_biblio;
272
273         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
274         $mock->mock( 'enqueue', sub {
275             my ( $self, $args ) = @_;
276             is_deeply(
277                 $args->{biblio_ids},
278                 [ $biblio->id ],
279                 '->fill triggers a holds queue update for the related biblio'
280             );
281         } );
282
283         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
284
285         $builder->build_object(
286             {
287                 class => 'Koha::Holds',
288                 value => {
289                     biblionumber   => $biblio->id,
290                 }
291             }
292         )->fill;
293
294         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
295         # this call shouldn't add a new test
296         $builder->build_object(
297             {
298                 class => 'Koha::Holds',
299                 value => {
300                     biblionumber   => $biblio->id,
301                 }
302             }
303         )->fill;
304     };
305
306     $schema->storage->txn_rollback;
307 };
308
309 subtest 'patron() tests' => sub {
310
311     plan tests => 2;
312
313     $schema->storage->txn_begin;
314
315     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
316     my $hold   = $builder->build_object(
317         {
318             class => 'Koha::Holds',
319             value => {
320                 borrowernumber => $patron->borrowernumber
321             }
322         }
323     );
324
325     my $hold_patron = $hold->patron;
326     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
327     is( $hold_patron->id, $patron->id, 'Right object' );
328
329     $schema->storage->txn_rollback;
330 };
331
332 subtest 'set_pickup_location() tests' => sub {
333
334     plan tests => 11;
335
336     $schema->storage->txn_begin;
337
338     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
339     my $mock_item   = Test::MockModule->new('Koha::Item');
340
341     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
342     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
343     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
344
345     # let's control what Koha::Biblio->pickup_locations returns, for testing
346     $mock_biblio->mock( 'pickup_locations', sub {
347         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
348     });
349     # let's mock what Koha::Item->pickup_locations returns, for testing
350     $mock_item->mock( 'pickup_locations', sub {
351         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
352     });
353
354     my $biblio = $builder->build_sample_biblio;
355     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
356
357     # Test biblio-level holds
358     my $biblio_hold = $builder->build_object(
359         {
360             class => "Koha::Holds",
361             value => {
362                 biblionumber => $biblio->biblionumber,
363                 branchcode   => $library_3->branchcode,
364                 itemnumber   => undef,
365             }
366         }
367     );
368
369     throws_ok
370         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
371         'Koha::Exceptions::Hold::InvalidPickupLocation',
372         'Exception thrown on invalid pickup location';
373
374     $biblio_hold->discard_changes;
375     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
376
377     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
378     is( ref($ret), 'Koha::Hold', 'self is returned' );
379
380     $biblio_hold->discard_changes;
381     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
382
383     # Test item-level holds
384     my $item_hold = $builder->build_object(
385         {
386             class => "Koha::Holds",
387             value => {
388                 biblionumber => $biblio->biblionumber,
389                 branchcode   => $library_3->branchcode,
390                 itemnumber   => $item->itemnumber,
391             }
392         }
393     );
394
395     throws_ok
396         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
397         'Koha::Exceptions::Hold::InvalidPickupLocation',
398         'Exception thrown on invalid pickup location';
399
400     $item_hold->discard_changes;
401     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
402
403     $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
404     $item_hold->discard_changes;
405     is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
406
407     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
408     is( ref($ret), 'Koha::Hold', 'self is returned' );
409
410     $item_hold->discard_changes;
411     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
412
413     throws_ok
414         { $item_hold->set_pickup_location({ library_id => undef }); }
415         'Koha::Exceptions::MissingParameter',
416         'Exception thrown if missing parameter';
417
418     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
419
420     $schema->storage->txn_rollback;
421 };
422
423 subtest 'is_pickup_location_valid() tests' => sub {
424
425     plan tests => 5;
426
427     $schema->storage->txn_begin;
428
429     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
430     my $mock_item   = Test::MockModule->new('Koha::Item');
431
432     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
433     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
434     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
435
436     # let's control what Koha::Biblio->pickup_locations returns, for testing
437     $mock_biblio->mock( 'pickup_locations', sub {
438         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
439     });
440     # let's mock what Koha::Item->pickup_locations returns, for testing
441     $mock_item->mock( 'pickup_locations', sub {
442         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
443     });
444
445     my $biblio = $builder->build_sample_biblio;
446     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
447
448     # Test biblio-level holds
449     my $biblio_hold = $builder->build_object(
450         {
451             class => "Koha::Holds",
452             value => {
453                 biblionumber => $biblio->biblionumber,
454                 branchcode   => $library_3->branchcode,
455                 itemnumber   => undef,
456             }
457         }
458     );
459
460     ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
461     ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
462
463     # Test item-level holds
464     my $item_hold = $builder->build_object(
465         {
466             class => "Koha::Holds",
467             value => {
468                 biblionumber => $biblio->biblionumber,
469                 branchcode   => $library_3->branchcode,
470                 itemnumber   => $item->itemnumber,
471             }
472         }
473     );
474
475     ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
476     ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
477
478     subtest 'pickup_locations() returning ->empty' => sub {
479
480         plan tests => 2;
481
482         $schema->storage->txn_begin;
483
484         my $library = $builder->build_object({ class => 'Koha::Libraries' });
485
486         my $mock_item = Test::MockModule->new('Koha::Item');
487         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
488
489         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
490         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
491
492         my $item   = $builder->build_sample_item();
493         my $biblio = $item->biblio;
494
495         # Test biblio-level holds
496         my $biblio_hold = $builder->build_object(
497             {
498                 class => "Koha::Holds",
499                 value => {
500                     biblionumber => $biblio->biblionumber,
501                     itemnumber   => undef,
502                 }
503             }
504         );
505
506         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
507
508         # Test item-level holds
509         my $item_hold = $builder->build_object(
510             {
511                 class => "Koha::Holds",
512                 value => {
513                     biblionumber => $biblio->biblionumber,
514                     itemnumber   => $item->itemnumber,
515                 }
516             }
517         );
518
519         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
520
521         $schema->storage->txn_rollback;
522     };
523
524     $schema->storage->txn_rollback;
525 };
526
527 subtest 'cancel() tests' => sub {
528
529     plan tests => 6;
530
531     $schema->storage->txn_begin;
532
533     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
534
535     # reduce the tests noise
536     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
537     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
538         undef );
539
540     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
541
542     # 0 == keep forever
543     $patron->privacy(0)->store;
544     my $hold = $builder->build_object(
545         {
546             class => 'Koha::Holds',
547             value => { borrowernumber => $patron->id, found => undef }
548         }
549     );
550     $hold->cancel();
551     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
552         $patron->borrowernumber, 'Patron link is kept' );
553
554     # 1 == "default", meaning it is not protected from removal
555     $patron->privacy(1)->store;
556     $hold = $builder->build_object(
557         {
558             class => 'Koha::Holds',
559             value => { borrowernumber => $patron->id, found => undef }
560         }
561     );
562     $hold->cancel();
563     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
564         $patron->borrowernumber, 'Patron link is kept' );
565
566     # 2 == delete immediately
567     $patron->privacy(2)->store;
568     $hold = $builder->build_object(
569         {
570             class => 'Koha::Holds',
571             value => { borrowernumber => $patron->id, found => undef }
572         }
573     );
574     throws_ok
575         { $hold->cancel(); }
576         'Koha::Exception',
577         'AnonymousPatron not set, exception thrown';
578
579     $hold->discard_changes;
580
581     ok( !$hold->is_found, 'Hold is not cancelled' );
582
583     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
584     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
585
586     $hold = $builder->build_object(
587         {
588             class => 'Koha::Holds',
589             value => { borrowernumber => $patron->id, found => undef }
590         }
591     );
592     $hold->cancel();
593     is(
594         Koha::Old::Holds->find( $hold->id )->borrowernumber,
595         $anonymous_patron->id,
596         'Patron link is set to the configured anonymous patron immediately'
597     );
598
599     subtest 'holds_queue update tests' => sub {
600
601         plan tests => 1;
602
603         my $biblio = $builder->build_sample_biblio;
604
605         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
606
607         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
608         $mock->mock( 'enqueue', sub {
609             my ( $self, $args ) = @_;
610             is_deeply(
611                 $args->{biblio_ids},
612                 [ $biblio->id ],
613                 '->cancel triggers a holds queue update for the related biblio'
614             );
615         } );
616
617         $builder->build_object(
618             {
619                 class => 'Koha::Holds',
620                 value => {
621                     biblionumber   => $biblio->id,
622                 }
623             }
624         )->cancel;
625
626         # If the skip_holds_queue param is not honoured, then test count will fail.
627         $builder->build_object(
628             {
629                 class => 'Koha::Holds',
630                 value => {
631                     biblionumber   => $biblio->id,
632                 }
633             }
634         )->cancel({ skip_holds_queue => 1 });
635
636         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
637
638         $builder->build_object(
639             {
640                 class => 'Koha::Holds',
641                 value => {
642                     biblionumber   => $biblio->id,
643                 }
644             }
645         )->cancel({ skip_holds_queue => 0 });
646     };
647
648     $schema->storage->txn_rollback;
649 };
650
651 subtest 'suspend_hold() and resume() tests' => sub {
652
653     plan tests => 2;
654
655     $schema->storage->txn_begin;
656
657     my $biblio = $builder->build_sample_biblio;
658     my $action;
659
660     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
661
662     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
663     $mock->mock( 'enqueue', sub {
664         my ( $self, $args ) = @_;
665         is_deeply(
666             $args->{biblio_ids},
667             [ $biblio->id ],
668             "->$action triggers a holds queue update for the related biblio"
669         );
670     } );
671
672     my $hold = $builder->build_object(
673         {
674             class => 'Koha::Holds',
675             value => {
676                 biblionumber => $biblio->id,
677                 found        => undef,
678             }
679         }
680     );
681
682     $action = 'suspend_hold';
683     $hold->suspend_hold;
684
685     $action = 'resume';
686     $hold->resume;
687
688     $schema->storage->txn_rollback;
689 };
690
691 subtest 'cancellation_requests() and add_cancellation_request() tests' => sub {
692
693     plan tests => 4;
694
695     $schema->storage->txn_begin;
696
697     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
698
699     my $hold = $builder->build_object( { class => 'Koha::Holds', } );
700
701     is( $hold->cancellation_requests->count, 0 );
702
703     # Add two cancellation requests
704     my $request_1 = $hold->add_cancellation_request;
705     isnt( $request_1->creation_date, undef, 'creation_date is set' );
706
707     my $requester     = $builder->build_object( { class => 'Koha::Patrons' } );
708     my $creation_date = '2021-06-25 14:05:35';
709
710     my $request_2 = $hold->add_cancellation_request(
711         {
712             creation_date => $creation_date,
713         }
714     );
715
716     is( $request_2->creation_date, $creation_date, 'Passed creation_date set' );
717
718     is( $hold->cancellation_requests->count, 2 );
719
720     $schema->storage->txn_rollback;
721 };
722
723 subtest 'cancellation_requestable_from_opac() tests' => sub {
724
725     plan tests => 5;
726
727     $schema->storage->txn_begin;
728
729     my $category =
730       $builder->build_object( { class => 'Koha::Patron::Categories' } );
731     my $item_home_library =
732       $builder->build_object( { class => 'Koha::Libraries' } );
733     my $patron_home_library =
734       $builder->build_object( { class => 'Koha::Libraries' } );
735
736     my $item =
737       $builder->build_sample_item( { library => $item_home_library->id } );
738     my $patron = $builder->build_object(
739         {
740             class => 'Koha::Patrons',
741             value => { branchcode => $patron_home_library->id }
742         }
743     );
744
745     subtest 'Exception cases' => sub {
746
747         plan tests => 4;
748
749         my $hold = $builder->build_object(
750             {
751                 class => 'Koha::Holds',
752                 value => {
753                     itemnumber     => undef,
754                     found          => undef,
755                     borrowernumber => $patron->id
756                 }
757             }
758         );
759
760         throws_ok { $hold->cancellation_requestable_from_opac; }
761         'Koha::Exceptions::InvalidStatus',
762           'Exception thrown because hold is not waiting';
763
764         is( $@->invalid_status, 'hold_not_waiting' );
765
766         $hold = $builder->build_object(
767             {
768                 class => 'Koha::Holds',
769                 value => {
770                     itemnumber     => undef,
771                     found          => 'W',
772                     borrowernumber => $patron->id
773                 }
774             }
775         );
776
777         throws_ok { $hold->cancellation_requestable_from_opac; }
778         'Koha::Exceptions::InvalidStatus',
779           'Exception thrown because waiting hold has no item linked';
780
781         is( $@->invalid_status, 'no_item_linked' );
782     };
783
784     # set default rule to enabled
785     Koha::CirculationRules->set_rule(
786         {
787             categorycode => '*',
788             itemtype     => '*',
789             branchcode   => '*',
790             rule_name    => 'waiting_hold_cancellation',
791             rule_value   => 1,
792         }
793     );
794
795     my $hold = $builder->build_object(
796         {
797             class => 'Koha::Holds',
798             value => {
799                 itemnumber     => $item->id,
800                 found          => 'W',
801                 borrowernumber => $patron->id
802             }
803         }
804     );
805
806     t::lib::Mocks::mock_preference( 'ReservesControlBranch',
807         'ItemHomeLibrary' );
808
809     Koha::CirculationRules->set_rule(
810         {
811             categorycode => $patron->categorycode,
812             itemtype     => $item->itype,
813             branchcode   => $item->homebranch,
814             rule_name    => 'waiting_hold_cancellation',
815             rule_value   => 0,
816         }
817     );
818
819     ok( !$hold->cancellation_requestable_from_opac );
820
821     Koha::CirculationRules->set_rule(
822         {
823             categorycode => $patron->categorycode,
824             itemtype     => $item->itype,
825             branchcode   => $item->homebranch,
826             rule_name    => 'waiting_hold_cancellation',
827             rule_value   => 1,
828         }
829     );
830
831     ok(
832         $hold->cancellation_requestable_from_opac,
833         'Make sure it is picking the right circulation rule'
834     );
835
836     t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
837
838     Koha::CirculationRules->set_rule(
839         {
840             categorycode => $patron->categorycode,
841             itemtype     => $item->itype,
842             branchcode   => $patron->branchcode,
843             rule_name    => 'waiting_hold_cancellation',
844             rule_value   => 0,
845         }
846     );
847
848     ok( !$hold->cancellation_requestable_from_opac );
849
850     Koha::CirculationRules->set_rule(
851         {
852             categorycode => $patron->categorycode,
853             itemtype     => $item->itype,
854             branchcode   => $patron->branchcode,
855             rule_name    => 'waiting_hold_cancellation',
856             rule_value   => 1,
857         }
858     );
859
860     ok(
861         $hold->cancellation_requestable_from_opac,
862         'Make sure it is picking the right circulation rule'
863     );
864
865     $schema->storage->txn_rollback;
866 };
867
868 subtest 'can_update_pickup_location_opac() tests' => sub {
869
870     plan tests => 8;
871
872     $schema->storage->txn_begin;
873
874     my $hold = $builder->build_object(
875         {   class => 'Koha::Holds',
876             value => { found => undef, suspend => 0, suspend_until => undef, waitingdate => undef }
877         }
878     );
879
880     t::lib::Mocks::mock_preference( 'OPACAllowUserToChangeBranch', '' );
881     $hold->found(undef);
882     is( $hold->can_update_pickup_location_opac, 0, "Pending hold pickup can't be changed (No change allowed)" );
883
884     $hold->found('T');
885     is( $hold->can_update_pickup_location_opac, 0, "In transit hold pickup can't be changed (No change allowed)" );
886
887     $hold->found('W');
888     is( $hold->can_update_pickup_location_opac, 0, "Waiting hold pickup can't be changed (No change allowed)" );
889
890     $hold->found(undef);
891     my $dt = dt_from_string();
892
893     $hold->suspend_hold( $dt );
894     is( $hold->can_update_pickup_location_opac, 0, "Suspended hold pickup can't be changed (No change allowed)" );
895     $hold->resume();
896
897     t::lib::Mocks::mock_preference( 'OPACAllowUserToChangeBranch', 'pending,intransit,suspended' );
898     $hold->found(undef);
899     is( $hold->can_update_pickup_location_opac, 1, "Pending hold pickup can be changed (pending,intransit,suspended allowed)" );
900
901     $hold->found('T');
902     is( $hold->can_update_pickup_location_opac, 1, "In transit hold pickup can be changed (pending,intransit,suspended allowed)" );
903
904     $hold->found('W');
905     is( $hold->can_update_pickup_location_opac, 0, "Waiting hold pickup can't be changed (pending,intransit,suspended allowed)" );
906
907     $hold->found(undef);
908     $dt = dt_from_string();
909     $hold->suspend_hold( $dt );
910     is( $hold->can_update_pickup_location_opac, 1, "Suspended hold pickup can be changed (pending,intransit,suspended allowed)" );
911
912     $schema->storage->txn_rollback;
913 };
914
915 subtest 'Koha::Hold::item_group tests' => sub {
916
917     plan tests => 1;
918
919     $schema->storage->txn_begin;
920
921     my $library  = $builder->build_object( { class => 'Koha::Libraries' } );
922     my $category = $builder->build_object(
923         {
924             class => 'Koha::Patron::Categories',
925             value => { exclude_from_local_holds_priority => 0 }
926         }
927     );
928     my $patron = $builder->build_object(
929         {
930             class => "Koha::Patrons",
931             value => {
932                 branchcode   => $library->branchcode,
933                 categorycode => $category->categorycode
934             }
935         }
936     );
937     my $biblio = $builder->build_sample_biblio();
938
939     my $item_group =
940       Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
941
942     my $hold = $builder->build_object(
943         {
944             class => "Koha::Holds",
945             value => {
946                 borrowernumber => $patron->borrowernumber,
947                 biblionumber   => $biblio->biblionumber,
948                 priority       => 1,
949                 item_group_id  => $item_group->id,
950             }
951         }
952     );
953
954     is( $hold->item_group->id, $item_group->id, "Got correct item group" );
955
956     $schema->storage->txn_rollback;
957 };