Bug 30410: 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 => 5;
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 => 12;
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     $schema->storage->txn_rollback;
237 };
238
239 subtest 'patron() tests' => sub {
240
241     plan tests => 2;
242
243     $schema->storage->txn_begin;
244
245     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
246     my $hold   = $builder->build_object(
247         {
248             class => 'Koha::Holds',
249             value => {
250                 borrowernumber => $patron->borrowernumber
251             }
252         }
253     );
254
255     my $hold_patron = $hold->patron;
256     is( ref($hold_patron), 'Koha::Patron', 'Right type' );
257     is( $hold_patron->id, $patron->id, 'Right object' );
258
259     $schema->storage->txn_rollback;
260 };
261
262 subtest 'set_pickup_location() tests' => sub {
263
264     plan tests => 11;
265
266     $schema->storage->txn_begin;
267
268     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
269     my $mock_item   = Test::MockModule->new('Koha::Item');
270
271     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
272     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
273     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
274
275     # let's control what Koha::Biblio->pickup_locations returns, for testing
276     $mock_biblio->mock( 'pickup_locations', sub {
277         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
278     });
279     # let's mock what Koha::Item->pickup_locations returns, for testing
280     $mock_item->mock( 'pickup_locations', sub {
281         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
282     });
283
284     my $biblio = $builder->build_sample_biblio;
285     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
286
287     # Test biblio-level holds
288     my $biblio_hold = $builder->build_object(
289         {
290             class => "Koha::Holds",
291             value => {
292                 biblionumber => $biblio->biblionumber,
293                 branchcode   => $library_3->branchcode,
294                 itemnumber   => undef,
295             }
296         }
297     );
298
299     throws_ok
300         { $biblio_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
301         'Koha::Exceptions::Hold::InvalidPickupLocation',
302         'Exception thrown on invalid pickup location';
303
304     $biblio_hold->discard_changes;
305     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
306
307     my $ret = $biblio_hold->set_pickup_location({ library_id => $library_2->id });
308     is( ref($ret), 'Koha::Hold', 'self is returned' );
309
310     $biblio_hold->discard_changes;
311     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
312
313     # Test item-level holds
314     my $item_hold = $builder->build_object(
315         {
316             class => "Koha::Holds",
317             value => {
318                 biblionumber => $biblio->biblionumber,
319                 branchcode   => $library_3->branchcode,
320                 itemnumber   => $item->itemnumber,
321             }
322         }
323     );
324
325     throws_ok
326         { $item_hold->set_pickup_location({ library_id => $library_1->branchcode }); }
327         'Koha::Exceptions::Hold::InvalidPickupLocation',
328         'Exception thrown on invalid pickup location';
329
330     $item_hold->discard_changes;
331     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
332
333     $item_hold->set_pickup_location({ library_id => $library_1->branchcode, force => 1 });
334     $item_hold->discard_changes;
335     is( $item_hold->branchcode, $library_1->branchcode, 'branchcode changed because of \'force\'' );
336
337     $ret = $item_hold->set_pickup_location({ library_id => $library_2->id });
338     is( ref($ret), 'Koha::Hold', 'self is returned' );
339
340     $item_hold->discard_changes;
341     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
342
343     throws_ok
344         { $item_hold->set_pickup_location({ library_id => undef }); }
345         'Koha::Exceptions::MissingParameter',
346         'Exception thrown if missing parameter';
347
348     like( "$@", qr/The library_id parameter is mandatory/, 'Exception message is clear' );
349
350     $schema->storage->txn_rollback;
351 };
352
353 subtest 'is_pickup_location_valid() tests' => sub {
354
355     plan tests => 5;
356
357     $schema->storage->txn_begin;
358
359     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
360     my $mock_item   = Test::MockModule->new('Koha::Item');
361
362     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
363     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
364     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
365
366     # let's control what Koha::Biblio->pickup_locations returns, for testing
367     $mock_biblio->mock( 'pickup_locations', sub {
368         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
369     });
370     # let's mock what Koha::Item->pickup_locations returns, for testing
371     $mock_item->mock( 'pickup_locations', sub {
372         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
373     });
374
375     my $biblio = $builder->build_sample_biblio;
376     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
377
378     # Test biblio-level holds
379     my $biblio_hold = $builder->build_object(
380         {
381             class => "Koha::Holds",
382             value => {
383                 biblionumber => $biblio->biblionumber,
384                 branchcode   => $library_3->branchcode,
385                 itemnumber   => undef,
386             }
387         }
388     );
389
390     ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
391     ok( $biblio_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid');
392
393     # Test item-level holds
394     my $item_hold = $builder->build_object(
395         {
396             class => "Koha::Holds",
397             value => {
398                 biblionumber => $biblio->biblionumber,
399                 branchcode   => $library_3->branchcode,
400                 itemnumber   => $item->itemnumber,
401             }
402         }
403     );
404
405     ok( !$item_hold->is_pickup_location_valid({ library_id => $library_1->branchcode }), 'Pickup location invalid');
406     ok( $item_hold->is_pickup_location_valid({ library_id => $library_2->id }), 'Pickup location valid' );
407
408     subtest 'pickup_locations() returning ->empty' => sub {
409
410         plan tests => 2;
411
412         $schema->storage->txn_begin;
413
414         my $library = $builder->build_object({ class => 'Koha::Libraries' });
415
416         my $mock_item = Test::MockModule->new('Koha::Item');
417         $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
418
419         my $mock_biblio = Test::MockModule->new('Koha::Biblio');
420         $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->new->empty; } );
421
422         my $item   = $builder->build_sample_item();
423         my $biblio = $item->biblio;
424
425         # Test biblio-level holds
426         my $biblio_hold = $builder->build_object(
427             {
428                 class => "Koha::Holds",
429                 value => {
430                     biblionumber => $biblio->biblionumber,
431                     itemnumber   => undef,
432                 }
433             }
434         );
435
436         ok( !$biblio_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
437
438         # Test item-level holds
439         my $item_hold = $builder->build_object(
440             {
441                 class => "Koha::Holds",
442                 value => {
443                     biblionumber => $biblio->biblionumber,
444                     itemnumber   => $item->itemnumber,
445                 }
446             }
447         );
448
449         ok( !$item_hold->is_pickup_location_valid({ library_id => $library->branchcode }), 'Pickup location invalid');
450
451         $schema->storage->txn_rollback;
452     };
453
454     $schema->storage->txn_rollback;
455 };
456
457 subtest 'cancel() tests' => sub {
458
459     plan tests => 5;
460
461     $schema->storage->txn_begin;
462
463     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
464
465     # reduce the tests noise
466     t::lib::Mocks::mock_preference( 'HoldsLog', 0 );
467     t::lib::Mocks::mock_preference( 'ExpireReservesMaxPickUpDelayCharge',
468         undef );
469
470     t::lib::Mocks::mock_preference( 'AnonymousPatron', undef );
471
472     # 0 == keep forever
473     $patron->privacy(0)->store;
474     my $hold = $builder->build_object(
475         {
476             class => 'Koha::Holds',
477             value => { borrowernumber => $patron->id, found => undef }
478         }
479     );
480     $hold->cancel();
481     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
482         $patron->borrowernumber, 'Patron link is kept' );
483
484     # 1 == "default", meaning it is not protected from removal
485     $patron->privacy(1)->store;
486     $hold = $builder->build_object(
487         {
488             class => 'Koha::Holds',
489             value => { borrowernumber => $patron->id, found => undef }
490         }
491     );
492     $hold->cancel();
493     is( Koha::Old::Holds->find( $hold->id )->borrowernumber,
494         $patron->borrowernumber, 'Patron link is kept' );
495
496     # 2 == delete immediately
497     $patron->privacy(2)->store;
498     $hold = $builder->build_object(
499         {
500             class => 'Koha::Holds',
501             value => { borrowernumber => $patron->id, found => undef }
502         }
503     );
504     throws_ok
505         { $hold->cancel(); }
506         'Koha::Exception',
507         'AnonymousPatron not set, exception thrown';
508
509     $hold->discard_changes;
510
511     ok( !$hold->is_found, 'Hold is not cancelled' );
512
513     my $anonymous_patron = $builder->build_object({ class => 'Koha::Patrons' });
514     t::lib::Mocks::mock_preference( 'AnonymousPatron', $anonymous_patron->id );
515
516     $hold = $builder->build_object(
517         {
518             class => 'Koha::Holds',
519             value => { borrowernumber => $patron->id, found => undef }
520         }
521     );
522     $hold->cancel();
523     is(
524         Koha::Old::Holds->find( $hold->id )->borrowernumber,
525         $anonymous_patron->id,
526         'Patron link is set to the configured anonymous patron immediately'
527     );
528
529     $schema->storage->txn_rollback;
530 };