Bug 30728: Add unit 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 => 6;
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::Holds;
33 use Koha::Libraries;
34 use Koha::Old::Holds;
35
36 my $schema  = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
38
39 subtest 'fill() tests' => sub {
40
41     plan tests => 13;
42
43     $schema->storage->txn_begin;
44
45     my $fee = 15;
46
47     my $category = $builder->build_object(
48         {
49             class => 'Koha::Patron::Categories',
50             value => { reservefee => $fee }
51         }
52     );
53     my $patron = $builder->build_object(
54         {
55             class => 'Koha::Patrons',
56             value => { categorycode => $category->id }
57         }
58     );
59     my $manager = $builder->build_object( { class => 'Koha::Patrons' } );
60
61     my $title  = 'Do what you want';
62     my $biblio = $builder->build_sample_biblio( { title => $title } );
63     my $item   = $builder->build_sample_item( { biblionumber => $biblio->id } );
64     my $hold   = $builder->build_object(
65         {
66             class => 'Koha::Holds',
67             value => {
68                 biblionumber   => $biblio->id,
69                 borrowernumber => $patron->id,
70                 itemnumber     => $item->id,
71                 priority       => 10,
72             }
73         }
74     );
75
76     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'any_time_is_collected' );
77     t::lib::Mocks::mock_preference( 'HoldsLog',    1 );
78     t::lib::Mocks::mock_userenv(
79         { patron => $manager, branchcode => $manager->branchcode } );
80
81     my $interface = 'api';
82     C4::Context->interface($interface);
83
84     my $ret = $hold->fill;
85
86     is( ref($ret), 'Koha::Hold', '->fill returns the object type' );
87     is( $ret->id, $hold->id, '->fill returns the object' );
88
89     is( Koha::Holds->find($hold->id), undef, 'Hold no longer current' );
90     my $old_hold = Koha::Old::Holds->find( $hold->id );
91
92     is( $old_hold->id, $hold->id, 'reserve_id retained' );
93     is( $old_hold->priority, 0, 'priority set to 0' );
94     is( $old_hold->found, 'F', 'found set to F' );
95
96     subtest 'fee applied tests' => sub {
97
98         plan tests => 9;
99
100         my $account = $patron->account;
101         is( $account->balance, $fee, 'Charge applied correctly' );
102
103         my $debits = $account->outstanding_debits;
104         is( $debits->count, 1, 'Only one fee charged' );
105
106         my $fee_debit = $debits->next;
107         is( $fee_debit->amount * 1, $fee, 'Fee amount stored correctly' );
108         is( $fee_debit->description, $title,
109             'Fee description stored correctly' );
110         is( $fee_debit->manager_id, $manager->id,
111             'Fee manager_id stored correctly' );
112         is( $fee_debit->branchcode, $manager->branchcode,
113             'Fee branchcode stored correctly' );
114         is( $fee_debit->interface, $interface,
115             'Fee interface stored correctly' );
116         is( $fee_debit->debit_type_code,
117             'RESERVE', 'Fee debit_type_code stored correctly' );
118         is( $fee_debit->itemnumber, $item->id,
119             'Fee itemnumber stored correctly' );
120     };
121
122     my $logs = Koha::ActionLogs->search(
123         {
124             action => 'FILL',
125             module => 'HOLDS',
126             object => $hold->id
127         }
128     );
129
130     is( $logs->count, 1, '1 log line added' );
131
132     # Set HoldFeeMode to something other than any_time_is_collected
133     t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
134     # Disable logging
135     t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
136
137     $hold = $builder->build_object(
138         {
139             class => 'Koha::Holds',
140             value => {
141                 biblionumber   => $biblio->id,
142                 borrowernumber => $patron->id,
143                 itemnumber     => $item->id,
144                 priority       => 10,
145             }
146         }
147     );
148
149     $hold->fill;
150
151     my $account = $patron->account;
152     is( $account->balance, $fee, 'No new charge applied' );
153
154     my $debits = $account->outstanding_debits;
155     is( $debits->count, 1, 'Only one fee charged, because of HoldFeeMode' );
156
157     $logs = Koha::ActionLogs->search(
158         {
159             action => 'FILL',
160             module => 'HOLDS',
161             object => $hold->id
162         }
163     );
164
165     is( $logs->count, 0, 'HoldsLog disabled, no logs added' );
166
167     subtest 'anonymization behavior tests' => sub {
168
169         plan tests => 5;
170
171         # reduce the tests noise
172         t::lib::Mocks::mock_preference( 'HoldsLog',    0 );
173         t::lib::Mocks::mock_preference( 'HoldFeeMode', 'not_always' );
174         # unset AnonymousPatron
175         t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
176
177         # 0 == keep forever
178         $patron->privacy(0)->store;
179         my $hold = $builder->build_object(
180             {
181                 class => 'Koha::Holds',
182                 value => { borrowernumber => $patron->id, found => undef }
183             }
184         );
185         $hold->fill();
186         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
187             $patron->borrowernumber, 'Patron link is kept' );
188
189         # 1 == "default", meaning it is not protected from removal
190         $patron->privacy(1)->store;
191         $hold = $builder->build_object(
192             {
193                 class => 'Koha::Holds',
194                 value => { borrowernumber => $patron->id, found => undef }
195             }
196         );
197         $hold->fill();
198         is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
199             $patron->borrowernumber, 'Patron link is kept' );
200
201         # 2 == delete immediately
202         $patron->privacy(2)->store;
203         $hold = $builder->build_object(
204             {
205                 class => 'Koha::Holds',
206                 value => { borrowernumber => $patron->id, found => undef }
207             }
208         );
209
210         throws_ok
211             { $hold->fill(); }
212             'Koha::Exception',
213             'AnonymousPatron not set, exception thrown';
214
215         $hold->discard_changes; # refresh from DB
216
217         ok( !$hold->is_found, 'Hold is not filled' );
218
219         my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
220         t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
221
222         $hold = $builder->build_object(
223             {
224                 class => 'Koha::Holds',
225                 value => { borrowernumber => $patron->id, found => undef }
226             }
227         );
228         $hold->fill();
229         is(
230             Koha::Old::Holds->find( $hold->id )->borrowernumber,
231             $anonymous_patron->id,
232             'Patron link is set to the configured anonymous patron immediately'
233         );
234     };
235
236     subtest 'holds_queue update tests' => sub {
237
238         plan tests => 1;
239
240         my $biblio = $builder->build_sample_biblio;
241
242         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
243         $mock->mock( 'enqueue', sub {
244             my ( $self, $args ) = @_;
245             is_deeply(
246                 $args->{biblio_ids},
247                 [ $biblio->id ],
248                 '->fill triggers a holds queue update for the related biblio'
249             );
250         } );
251
252         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
253
254         $builder->build_object(
255             {
256                 class => 'Koha::Holds',
257                 value => {
258                     biblionumber   => $biblio->id,
259                 }
260             }
261         )->fill;
262
263         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
264         # this call shouldn't add a new test
265         $builder->build_object(
266             {
267                 class => 'Koha::Holds',
268                 value => {
269                     biblionumber   => $biblio->id,
270                 }
271             }
272         )->fill;
273     };
274
275     $schema->storage->txn_rollback;
276 };
277
278 subtest 'patron() tests' => sub {
279
280     plan tests => 2;
281
282     $schema->storage->txn_begin;
283
284     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
285     my $hold   = $builder->build_object(
286         {
287             class => 'Koha::Holds',
288             value => {
289                 borrowernumber => $patron->borrowernumber
290             }
291         }
292     );
293
294     my $hold_patron = $hold->patron;
295     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
296     is( $hold_patron->id, $patron->id, 'Right object' );
297
298     $schema->storage->txn_rollback;
299 };
300
301 subtest 'set_pickup_location() tests' => sub {
302
303     plan tests => 11;
304
305     $schema->storage->txn_begin;
306
307     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
308     my $mock_item   = Test::MockModule->new('Koha::Item');
309
310     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
311     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
312     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
313
314     # let's control what Koha::Biblio->pickup_locations returns, for testing
315     $mock_biblio->mock( 'pickup_locations', sub {
316         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
317     });
318     # let's mock what Koha::Item->pickup_locations returns, for testing
319     $mock_item->mock( 'pickup_locations', sub {
320         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
321     });
322
323     my $biblio = $builder->build_sample_biblio;
324     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
325
326     # Test biblio-level holds
327     my $biblio_hold = $builder->build_object(
328         {
329             class => "Koha::Holds",
330             value => {
331                 biblionumber => $biblio->biblionumber,
332                 branchcode   => $library_3->branchcode,
333                 itemnumber   => undef,
334             }
335         }
336     );
337
338     throws_ok
339         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
340         'Koha::Exceptions::Hold::InvalidPickupLocation',
341         'Exception thrown on invalid pickup location';
342
343     $biblio_hold->discard_changes;
344     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
345
346     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
347     is( ref($ret), 'Koha::Hold', 'self is returned' );
348
349     $biblio_hold->discard_changes;
350     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
351
352     # Test item-level holds
353     my $item_hold = $builder->build_object(
354         {
355             class => "Koha::Holds",
356             value => {
357                 biblionumber => $biblio->biblionumber,
358                 branchcode   => $library_3->branchcode,
359                 itemnumber   => $item->itemnumber,
360             }
361         }
362     );
363
364     throws_ok
365         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
366         'Koha::Exceptions::Hold::InvalidPickupLocation',
367         'Exception thrown on invalid pickup location';
368
369     $item_hold->discard_changes;
370     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
371
372     $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
373     $item_hold->discard_changes;
374     is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
375
376     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
377     is( ref($ret), 'Koha::Hold', 'self is returned' );
378
379     $item_hold->discard_changes;
380     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
381
382     throws_ok
383         { $item_hold->set_pickup_location({ library_id => undef }); }
384         'Koha::Exceptions::MissingParameter',
385         'Exception thrown if missing parameter';
386
387     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
388
389     $schema->storage->txn_rollback;
390 };
391
392 subtest 'is_pickup_location_valid() tests' => sub {
393
394     plan tests => 5;
395
396     $schema->storage->txn_begin;
397
398     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
399     my $mock_item   = Test::MockModule->new('Koha::Item');
400
401     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
402     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
403     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
404
405     # let's control what Koha::Biblio->pickup_locations returns, for testing
406     $mock_biblio->mock( 'pickup_locations', sub {
407         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
408     });
409     # let's mock what Koha::Item->pickup_locations returns, for testing
410     $mock_item->mock( 'pickup_locations', sub {
411         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
412     });
413
414     my $biblio = $builder->build_sample_biblio;
415     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
416
417     # Test biblio-level holds
418     my $biblio_hold = $builder->build_object(
419         {
420             class => "Koha::Holds",
421             value => {
422                 biblionumber => $biblio->biblionumber,
423                 branchcode   => $library_3->branchcode,
424                 itemnumber   => undef,
425             }
426         }
427     );
428
429     ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
430     ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
431
432     # Test item-level holds
433     my $item_hold = $builder->build_object(
434         {
435             class => "Koha::Holds",
436             value => {
437                 biblionumber => $biblio->biblionumber,
438                 branchcode   => $library_3->branchcode,
439                 itemnumber   => $item->itemnumber,
440             }
441         }
442     );
443
444     ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
445     ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
446
447     subtest 'pickup_locations() returning ->empty' => sub {
448
449         plan tests => 2;
450
451         $schema->storage->txn_begin;
452
453         my $library = $builder->build_object({ class => 'Koha::Libraries' });
454
455         my $mock_item = Test::MockModule->new('Koha::Item');
456         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
457
458         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
459         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
460
461         my $item   = $builder->build_sample_item();
462         my $biblio = $item->biblio;
463
464         # Test biblio-level holds
465         my $biblio_hold = $builder->build_object(
466             {
467                 class => "Koha::Holds",
468                 value => {
469                     biblionumber => $biblio->biblionumber,
470                     itemnumber   => undef,
471                 }
472             }
473         );
474
475         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
476
477         # Test item-level holds
478         my $item_hold = $builder->build_object(
479             {
480                 class => "Koha::Holds",
481                 value => {
482                     biblionumber => $biblio->biblionumber,
483                     itemnumber   => $item->itemnumber,
484                 }
485             }
486         );
487
488         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
489
490         $schema->storage->txn_rollback;
491     };
492
493     $schema->storage->txn_rollback;
494 };
495
496 subtest 'cancel() tests' => sub {
497
498     plan tests => 6;
499
500     $schema->storage->txn_begin;
501
502     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
503
504     # reduce the tests noise
505     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
506     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
507         undef );
508
509     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
510
511     # 0 == keep forever
512     $patron->privacy(0)->store;
513     my $hold = $builder->build_object(
514         {
515             class => 'Koha::Holds',
516             value => { borrowernumber => $patron->id, found => undef }
517         }
518     );
519     $hold->cancel();
520     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
521         $patron->borrowernumber, 'Patron link is kept' );
522
523     # 1 == "default", meaning it is not protected from removal
524     $patron->privacy(1)->store;
525     $hold = $builder->build_object(
526         {
527             class => 'Koha::Holds',
528             value => { borrowernumber => $patron->id, found => undef }
529         }
530     );
531     $hold->cancel();
532     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
533         $patron->borrowernumber, 'Patron link is kept' );
534
535     # 2 == delete immediately
536     $patron->privacy(2)->store;
537     $hold = $builder->build_object(
538         {
539             class => 'Koha::Holds',
540             value => { borrowernumber => $patron->id, found => undef }
541         }
542     );
543     throws_ok
544         { $hold->cancel(); }
545         'Koha::Exception',
546         'AnonymousPatron not set, exception thrown';
547
548     $hold->discard_changes;
549
550     ok( !$hold->is_found, 'Hold is not cancelled' );
551
552     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
553     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
554
555     $hold = $builder->build_object(
556         {
557             class => 'Koha::Holds',
558             value => { borrowernumber => $patron->id, found => undef }
559         }
560     );
561     $hold->cancel();
562     is(
563         Koha::Old::Holds->find( $hold->id )->borrowernumber,
564         $anonymous_patron->id,
565         'Patron link is set to the configured anonymous patron immediately'
566     );
567
568     subtest 'holds_queue update tests' => sub {
569
570         plan tests => 1;
571
572         my $biblio = $builder->build_sample_biblio;
573
574         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
575
576         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
577         $mock->mock( 'enqueue', sub {
578             my ( $self, $args ) = @_;
579             is_deeply(
580                 $args->{biblio_ids},
581                 [ $biblio->id ],
582                 '->cancel triggers a holds queue update for the related biblio'
583             );
584         } );
585
586         $builder->build_object(
587             {
588                 class => 'Koha::Holds',
589                 value => {
590                     biblionumber   => $biblio->id,
591                 }
592             }
593         )->cancel;
594
595         # If the skip_holds_queue param is not honoured, then test count will fail.
596         $builder->build_object(
597             {
598                 class => 'Koha::Holds',
599                 value => {
600                     biblionumber   => $biblio->id,
601                 }
602             }
603         )->cancel({ skip_holds_queue => 1 });
604
605         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
606
607         $builder->build_object(
608             {
609                 class => 'Koha::Holds',
610                 value => {
611                     biblionumber   => $biblio->id,
612                 }
613             }
614         )->cancel({ skip_holds_queue => 0 });
615     };
616
617     $schema->storage->txn_rollback;
618 };
619
620 subtest 'suspend_hold() and resume() tests' => sub {
621
622     plan tests => 2;
623
624     $schema->storage->txn_begin;
625
626     my $biblio = $builder->build_sample_biblio;
627     my $action;
628
629     t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
630
631     my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
632     $mock->mock( 'enqueue', sub {
633         my ( $self, $args ) = @_;
634         is_deeply(
635             $args->{biblio_ids},
636             [ $biblio->id ],
637             "->$action triggers a holds queue update for the related biblio"
638         );
639     } );
640
641     my $hold = $builder->build_object(
642         {
643             class => 'Koha::Holds',
644             value => {
645                 biblionumber => $biblio->id,
646                 found        => undef,
647             }
648         }
649     );
650
651     $action = 'suspend_hold';
652     $hold->suspend_hold;
653
654     $action = 'resume';
655     $hold->resume;
656
657     $schema->storage->txn_rollback;
658 };