Bug 27797: (QA follow-up) Make tests more robust
[srvgit] / t / db_dependent / api / v1 / holds.t
1 #!/usr/bin/env 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 Test::More tests => 13;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::TestBuilder;
24 use t::lib::Mocks;
25
26 use DateTime;
27 use Mojo::JSON qw(encode_json);
28
29 use C4::Context;
30 use Koha::Patrons;
31 use C4::Reserves;
32 use C4::Items;
33
34 use Koha::Database;
35 use Koha::DateUtils;
36 use Koha::Biblios;
37 use Koha::Biblioitems;
38 use Koha::Items;
39 use Koha::CirculationRules;
40
41 my $schema  = Koha::Database->new->schema;
42 my $builder = t::lib::TestBuilder->new();
43
44 $schema->storage->txn_begin;
45
46 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
47
48 my $t = Test::Mojo->new('Koha::REST::V1');
49
50 my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
51 my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
52 my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
53 my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype};
54
55 # Generic password for everyone
56 my $password = 'thePassword123';
57
58 # User without any permissions
59 my $nopermission = $builder->build_object({
60     class => 'Koha::Patrons',
61     value => {
62         branchcode   => $branchcode,
63         categorycode => $categorycode,
64         flags        => 0
65     }
66 });
67 $nopermission->set_password( { password => $password, skip_validation => 1 } );
68 my $nopermission_userid = $nopermission->userid;
69
70 my $patron_1 = $builder->build_object(
71     {
72         class => 'Koha::Patrons',
73         value => {
74             categorycode => $categorycode,
75             branchcode   => $branchcode,
76             surname      => 'Test Surname',
77             flags        => 80, #borrowers and reserveforothers flags
78         }
79     }
80 );
81 $patron_1->set_password( { password => $password, skip_validation => 1 } );
82 my $userid_1 = $patron_1->userid;
83
84 my $patron_2 = $builder->build_object(
85     {
86         class => 'Koha::Patrons',
87         value => {
88             categorycode => $categorycode,
89             branchcode   => $branchcode,
90             surname      => 'Test Surname 2',
91             flags        => 16, # borrowers flag
92         }
93     }
94 );
95 $patron_2->set_password( { password => $password, skip_validation => 1 } );
96 my $userid_2 = $patron_2->userid;
97
98 my $patron_3 = $builder->build_object(
99     {
100         class => 'Koha::Patrons',
101         value => {
102             categorycode => $categorycode,
103             branchcode   => $branchcode,
104             surname      => 'Test Surname 3',
105             flags        => 64, # reserveforothers flag
106         }
107     }
108 );
109 $patron_3->set_password( { password => $password, skip_validation => 1 } );
110 my $userid_3 = $patron_3->userid;
111
112 my $biblio_1 = $builder->build_sample_biblio;
113 my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber, itype => $itemtype });
114
115 my $biblio_2 = $builder->build_sample_biblio;
116 my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber, itype => $itemtype });
117
118 my $dbh = C4::Context->dbh;
119 $dbh->do('DELETE FROM reserves');
120 Koha::CirculationRules->search()->delete();
121 Koha::CirculationRules->set_rules(
122     {
123         categorycode => undef,
124         branchcode   => undef,
125         itemtype     => undef,
126         rules        => {
127             reservesallowed => 1,
128             holds_per_record => 99
129         }
130     }
131 );
132
133 my $reserve_id = C4::Reserves::AddReserve(
134     {
135         branchcode     => $branchcode,
136         borrowernumber => $patron_1->borrowernumber,
137         biblionumber   => $biblio_1->biblionumber,
138         priority       => 1,
139         itemnumber     => $item_1->itemnumber,
140     }
141 );
142
143 # Add another reserve to be able to change first reserve's rank
144 my $reserve_id2 = C4::Reserves::AddReserve(
145     {
146         branchcode     => $branchcode,
147         borrowernumber => $patron_2->borrowernumber,
148         biblionumber   => $biblio_1->biblionumber,
149         priority       => 2,
150         itemnumber     => $item_1->itemnumber,
151     }
152 );
153
154 my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' );
155 my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
156
157 my $post_data = {
158     patron_id => int($patron_1->borrowernumber),
159     biblio_id => int($biblio_1->biblionumber),
160     item_id => int($item_1->itemnumber),
161     pickup_library_id => $branchcode,
162     expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
163     priority => 2,
164 };
165 my $put_data = {
166     priority => 2,
167     suspended_until => output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }),
168 };
169
170 subtest "Test endpoints without authentication" => sub {
171     plan tests => 8;
172     $t->get_ok('/api/v1/holds')
173       ->status_is(401);
174     $t->post_ok('/api/v1/holds')
175       ->status_is(401);
176     $t->put_ok('/api/v1/holds/0')
177       ->status_is(401);
178     $t->delete_ok('/api/v1/holds/0')
179       ->status_is(401);
180 };
181
182 subtest "Test endpoints without permission" => sub {
183
184     plan tests => 10;
185
186     $t->get_ok( "//$nopermission_userid:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber ) # no permission
187       ->status_is(403);
188
189     $t->get_ok( "//$userid_3:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )    # no permission
190       ->status_is(403);
191
192     $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data )
193       ->status_is(403);
194
195     $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_data )
196       ->status_is(403);
197
198     $t->delete_ok( "//$nopermission_userid:$password@/api/v1/holds/0" )
199       ->status_is(403);
200 };
201
202 subtest "Test endpoints with permission" => sub {
203
204     plan tests => 44;
205
206     $t->get_ok( "//$userid_1:$password@/api/v1/holds" )
207       ->status_is(200)
208       ->json_has('/0')
209       ->json_has('/1')
210       ->json_hasnt('/2');
211
212     $t->get_ok( "//$userid_1:$password@/api/v1/holds?priority=2" )
213       ->status_is(200)
214       ->json_is('/0/patron_id', $patron_2->borrowernumber)
215       ->json_hasnt('/1');
216
217     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
218       ->status_is(204, 'SWAGGER3.2.4')
219       ->content_is('', 'SWAGGER3.3.4');
220
221     $t->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data )
222       ->status_is(404)
223       ->json_has('/error');
224
225     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
226       ->status_is(404)
227       ->json_has('/error');
228
229     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
230       ->status_is(200)
231       ->json_is([]);
232
233     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
234     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
235       ->status_is(200)
236       ->json_is([]);
237
238     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
239       ->status_is(204, 'SWAGGER3.2.4')
240       ->content_is('', 'SWAGGER3.3.4');
241
242     # Make sure pickup location checks doesn't get in the middle
243     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
244     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
245     my $mock_item   = Test::MockModule->new('Koha::Item');
246     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
247
248     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
249       ->status_is(201)
250       ->json_has('/hold_id');
251
252     # Get id from response
253     $reserve_id = $t->tx->res->json->{hold_id};
254
255     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
256       ->status_is(200)
257       ->json_is('/0/hold_id', $reserve_id)
258       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
259       ->json_is('/0/pickup_library_id', $branchcode);
260
261     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
262       ->status_is(403)
263       ->json_like('/error', qr/itemAlreadyOnHold/);
264
265     $post_data->{biblionumber} = int($biblio_2->biblionumber);
266     $post_data->{itemnumber}   = int($item_2->itemnumber);
267
268     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
269       ->status_is(403)
270       ->json_like('/error', qr/itemAlreadyOnHold/);
271
272     my $to_delete_patron  = $builder->build_object({ class => 'Koha::Patrons' });
273     my $deleted_patron_id = $to_delete_patron->borrowernumber;
274     $to_delete_patron->delete;
275
276     my $tmp_patron_id = $post_data->{patron_id};
277     $post_data->{patron_id} = $deleted_patron_id;
278     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
279       ->status_is(400)
280       ->json_is( { error => 'patron_id not found' } );
281
282     # Restore the original patron_id as it is expected by the next subtest
283     # FIXME: this tests need to be rewritten from scratch
284     $post_data->{patron_id} = $tmp_patron_id;
285 };
286
287 subtest 'Reserves with itemtype' => sub {
288     plan tests => 10;
289
290     my $post_data = {
291         patron_id => int($patron_1->borrowernumber),
292         biblio_id => int($biblio_1->biblionumber),
293         pickup_library_id => $branchcode,
294         item_type => $itemtype,
295     };
296
297     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
298       ->status_is(204, 'SWAGGER3.2.4')
299       ->content_is('', 'SWAGGER3.3.4');
300
301     # Make sure pickup location checks doesn't get in the middle
302     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
303     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
304     my $mock_item   = Test::MockModule->new('Koha::Item');
305     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
306
307     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
308       ->status_is(201)
309       ->json_has('/hold_id');
310
311     $reserve_id = $t->tx->res->json->{hold_id};
312
313     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
314       ->status_is(200)
315       ->json_is('/0/hold_id', $reserve_id)
316       ->json_is('/0/item_type', $itemtype);
317 };
318
319
320 subtest 'test AllowHoldDateInFuture' => sub {
321
322     plan tests => 6;
323
324     $dbh->do('DELETE FROM reserves');
325
326     my $future_hold_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
327
328     my $post_data = {
329         patron_id => int($patron_1->borrowernumber),
330         biblio_id => int($biblio_1->biblionumber),
331         item_id => int($item_1->itemnumber),
332         pickup_library_id => $branchcode,
333         expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
334         hold_date => output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }),
335         priority => 2,
336     };
337
338     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 );
339
340     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
341       ->status_is(400)
342       ->json_has('/error');
343
344     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 );
345
346     # Make sure pickup location checks doesn't get in the middle
347     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
348     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
349     my $mock_item   = Test::MockModule->new('Koha::Item');
350     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
351
352     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
353       ->status_is(201)
354       ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }));
355 };
356
357 $schema->storage->txn_rollback;
358
359 subtest 'x-koha-override and AllowHoldPolicyOverride tests' => sub {
360
361     plan tests => 12;
362
363     $schema->storage->txn_begin;
364
365     my $patron = $builder->build_object(
366         {
367             class => 'Koha::Patrons',
368             value => { flags => 1 }
369         }
370     );
371     my $password = 'thePassword123';
372     $patron->set_password( { password => $password, skip_validation => 1 } );
373     $patron->discard_changes;
374     my $userid = $patron->userid;
375
376     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
377
378     # Make sure pickup location checks doesn't get in the middle
379     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
380     $mock_biblio->mock( 'pickup_locations',
381         sub { return Koha::Libraries->search; } );
382     my $mock_item = Test::MockModule->new('Koha::Item');
383     $mock_item->mock( 'pickup_locations',
384         sub { return Koha::Libraries->search } );
385
386     my $can_item_be_reserved_result;
387     my $mock_reserves = Test::MockModule->new('C4::Reserves');
388     $mock_reserves->mock(
389         'CanItemBeReserved',
390         sub {
391             return $can_item_be_reserved_result;
392         }
393     );
394
395     my $item = $builder->build_sample_item;
396
397     my $post_data = {
398         item_id           => $item->id,
399         biblio_id         => $item->biblionumber,
400         patron_id         => $patron->id,
401         pickup_library_id => $patron->branchcode,
402     };
403
404     $can_item_be_reserved_result = { status => 'ageRestricted' };
405
406     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
407       ->status_is(403)
408       ->json_is( '/error' => "Hold cannot be placed. Reason: ageRestricted" );
409
410     # x-koha-override doesn't override if AllowHoldPolicyOverride not set
411     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
412           { 'x-koha-override' => 'any' } => json => $post_data )
413       ->status_is(403);
414
415     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
416
417     $can_item_be_reserved_result = { status => 'pickupNotInHoldGroup' };
418
419     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
420       ->status_is(403)
421       ->json_is(
422         '/error' => "Hold cannot be placed. Reason: pickupNotInHoldGroup" );
423
424     # x-koha-override overrides the status
425     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
426           { 'x-koha-override' => 'any' } => json => $post_data )
427       ->status_is(201);
428
429     $can_item_be_reserved_result = { status => 'OK' };
430
431     # x-koha-override works when status not need override
432     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
433           { 'x-koha-override' => 'any' } => json => $post_data )
434       ->status_is(201);
435
436     $schema->storage->txn_rollback;
437 };
438
439 subtest 'suspend and resume tests' => sub {
440
441     plan tests => 24;
442
443     $schema->storage->txn_begin;
444
445     my $password = 'AbcdEFG123';
446
447     my $patron = $builder->build_object(
448         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
449     $patron->set_password({ password => $password, skip_validation => 1 });
450     my $userid = $patron->userid;
451
452     # Disable logging
453     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
454     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
455
456     my $hold = $builder->build_object(
457         {   class => 'Koha::Holds',
458             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
459         }
460     );
461
462     ok( !$hold->is_suspended, 'Hold is not suspended' );
463     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
464         ->status_is( 201, 'Hold suspension created' );
465
466     $hold->discard_changes;    # refresh object
467
468     ok( $hold->is_suspended, 'Hold is suspended' );
469     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
470
471     my $end_date = output_pref({
472       dt         => dt_from_string( undef ),
473       dateformat => 'rfc3339',
474       dateonly   => 1
475     });
476
477     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
478
479     $hold->discard_changes;    # refresh object
480
481     ok( $hold->is_suspended, 'Hold is suspended' );
482     $t->json_is(
483       '/end_date',
484       output_pref({
485         dt         => dt_from_string( $hold->suspend_until ),
486         dateformat => 'rfc3339',
487         dateonly   => 1
488       }),
489       'Hold suspension has correct end date'
490     );
491
492     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
493       ->status_is(204, 'SWAGGER3.2.4')
494       ->content_is('', 'SWAGGER3.3.4');
495
496     # Pass a an expiration date for the suspension
497     my $date = dt_from_string()->add( days => 5 );
498     $t->post_ok(
499               "//$userid:$password@/api/v1/holds/"
500             . $hold->id
501             . "/suspension" => json => {
502             end_date =>
503                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
504             }
505     )->status_is( 201, 'Hold suspension created' )
506         ->json_is( '/end_date',
507         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
508         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
509
510     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
511       ->status_is(204, 'SWAGGER3.2.4')
512       ->content_is('', 'SWAGGER3.3.4');
513
514     $hold->set_waiting->discard_changes;
515
516     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
517       ->status_is( 400, 'Cannot suspend waiting hold' )
518       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
519
520     $hold->set_transfer->discard_changes;
521
522     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
523       ->status_is( 400, 'Cannot suspend hold on transfer' )
524       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
525
526     $schema->storage->txn_rollback;
527 };
528
529 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
530
531     plan tests => 14;
532
533     $schema->storage->txn_begin;
534
535     my $password = 'AbcdEFG123';
536
537     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
538     my $patron_np = $builder->build_object(
539         { class => 'Koha::Patrons', value => { flags => 0 } } );
540     $patron_np->set_password( { password => $password, skip_validation => 1 } );
541     my $userid_np = $patron_np->userid;
542
543     my $patron = $builder->build_object(
544         { class => 'Koha::Patrons', value => { flags => 0 } } );
545     $patron->set_password( { password => $password, skip_validation => 1 } );
546     my $userid = $patron->userid;
547     $builder->build(
548         {
549             source => 'UserPermission',
550             value  => {
551                 borrowernumber => $patron->borrowernumber,
552                 module_bit     => 6,
553                 code           => 'modify_holds_priority',
554             },
555         }
556     );
557
558     # Disable logging
559     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
560     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
561
562     my $biblio   = $builder->build_sample_biblio;
563     my $patron_1 = $builder->build_object(
564         {
565             class => 'Koha::Patrons',
566             value => { branchcode => $library->branchcode }
567         }
568     );
569     my $patron_2 = $builder->build_object(
570         {
571             class => 'Koha::Patrons',
572             value => { branchcode => $library->branchcode }
573         }
574     );
575     my $patron_3 = $builder->build_object(
576         {
577             class => 'Koha::Patrons',
578             value => { branchcode => $library->branchcode }
579         }
580     );
581
582     my $hold_1 = Koha::Holds->find(
583         AddReserve(
584             {
585                 branchcode     => $library->branchcode,
586                 borrowernumber => $patron_1->borrowernumber,
587                 biblionumber   => $biblio->biblionumber,
588                 priority       => 1,
589             }
590         )
591     );
592     my $hold_2 = Koha::Holds->find(
593         AddReserve(
594             {
595                 branchcode     => $library->branchcode,
596                 borrowernumber => $patron_2->borrowernumber,
597                 biblionumber   => $biblio->biblionumber,
598                 priority       => 2,
599             }
600         )
601     );
602     my $hold_3 = Koha::Holds->find(
603         AddReserve(
604             {
605                 branchcode     => $library->branchcode,
606                 borrowernumber => $patron_3->borrowernumber,
607                 biblionumber   => $biblio->biblionumber,
608                 priority       => 3,
609             }
610         )
611     );
612
613     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
614           . $hold_3->id
615           . "/priority" => json => 1 )->status_is(403);
616
617     $t->put_ok( "//$userid:$password@/api/v1/holds/"
618           . $hold_3->id
619           . "/priority" => json => 1 )->status_is(200)->json_is(1);
620
621     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
622     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
623     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
624
625     $t->put_ok( "//$userid:$password@/api/v1/holds/"
626           . $hold_3->id
627           . "/priority" => json => 3 )->status_is(200)->json_is(3);
628
629     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
630     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
631     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
632
633     $schema->storage->txn_rollback;
634 };
635
636 subtest 'add() tests (maxreserves behaviour)' => sub {
637
638     plan tests => 7;
639
640     $schema->storage->txn_begin;
641
642     $dbh->do('DELETE FROM reserves');
643
644     Koha::CirculationRules->new->delete;
645
646     my $password = 'AbcdEFG123';
647
648     my $patron = $builder->build_object(
649         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
650     $patron->set_password({ password => $password, skip_validation => 1 });
651     my $userid = $patron->userid;
652
653     Koha::CirculationRules->set_rules(
654         {
655             itemtype     => undef,
656             branchcode   => undef,
657             categorycode => undef,
658             rules        => {
659                 reservesallowed => 3
660             }
661         }
662     );
663
664     Koha::CirculationRules->set_rules(
665         {
666             branchcode   => undef,
667             categorycode => $patron->categorycode,
668             rules        => {
669                 max_holds   => 4,
670             }
671         }
672     );
673
674     my $biblio_1 = $builder->build_sample_biblio;
675     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
676     my $biblio_2 = $builder->build_sample_biblio;
677     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
678     my $biblio_3 = $builder->build_sample_biblio;
679     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
680
681     # Make sure pickup location checks doesn't get in the middle
682     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
683     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
684     my $mock_item   = Test::MockModule->new('Koha::Item');
685     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
686
687     # Disable logging
688     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
689     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
690     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
691     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
692
693     my $post_data = {
694         patron_id => $patron->borrowernumber,
695         biblio_id => $biblio_1->biblionumber,
696         pickup_library_id => $item_1->home_branch->branchcode,
697         item_type => $item_1->itype,
698     };
699
700     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
701       ->status_is(201);
702
703     $post_data = {
704         patron_id => $patron->borrowernumber,
705         biblio_id => $biblio_2->biblionumber,
706         pickup_library_id => $item_2->home_branch->branchcode,
707         item_id   => $item_2->itemnumber
708     };
709
710     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
711       ->status_is(201);
712
713     $post_data = {
714         patron_id => $patron->borrowernumber,
715         biblio_id => $biblio_3->biblionumber,
716         pickup_library_id => $item_1->home_branch->branchcode,
717         item_id   => $item_3->itemnumber
718     };
719
720     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
721       ->status_is(403)
722       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
723
724     $schema->storage->txn_rollback;
725 };
726
727 subtest 'pickup_locations() tests' => sub {
728
729     plan tests => 12;
730
731     $schema->storage->txn_begin;
732
733     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
734
735     # Small trick to ease testing
736     Koha::Libraries->search->update({ pickup_location => 0 });
737
738     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } });
739     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } });
740     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } });
741
742     my $library_1_api = $library_1->to_api();
743     my $library_2_api = $library_2->to_api();
744     my $library_3_api = $library_3->to_api();
745
746     $library_1_api->{needs_override} = Mojo::JSON->false;
747     $library_2_api->{needs_override} = Mojo::JSON->false;
748     $library_3_api->{needs_override} = Mojo::JSON->false;
749
750     my $patron = $builder->build_object(
751         {
752             class => 'Koha::Patrons',
753             value => { userid => 'tomasito', flags => 0 }
754         }
755     );
756     $patron->set_password( { password => $password, skip_validation => 1 } );
757     my $userid = $patron->userid;
758     $builder->build(
759         {
760             source => 'UserPermission',
761             value  => {
762                 borrowernumber => $patron->borrowernumber,
763                 module_bit     => 6,
764                 code           => 'place_holds',
765             },
766         }
767     );
768
769     my $item_class = Test::MockModule->new('Koha::Item');
770     $item_class->mock(
771         'pickup_locations',
772         sub {
773             my ( $self, $params ) = @_;
774             my $mock_patron = $params->{patron};
775             is( $mock_patron->borrowernumber,
776                 $patron->borrowernumber, 'Patron passed correctly' );
777             return Koha::Libraries->search(
778                 {
779                     branchcode => {
780                         '-in' => [
781                             $library_1->branchcode,
782                             $library_2->branchcode
783                         ]
784                     }
785                 },
786                 {   # we make sure no surprises in the order of the result
787                     order_by => { '-asc' => 'marcorgcode' }
788                 }
789             );
790         }
791     );
792
793     my $biblio_class = Test::MockModule->new('Koha::Biblio');
794     $biblio_class->mock(
795         'pickup_locations',
796         sub {
797             my ( $self, $params ) = @_;
798             my $mock_patron = $params->{patron};
799             is( $mock_patron->borrowernumber,
800                 $patron->borrowernumber, 'Patron passed correctly' );
801             return Koha::Libraries->search(
802                 {
803                     branchcode => {
804                         '-in' => [
805                             $library_2->branchcode,
806                             $library_3->branchcode
807                         ]
808                     }
809                 },
810                 {   # we make sure no surprises in the order of the result
811                     order_by => { '-asc' => 'marcorgcode' }
812                 }
813             );
814         }
815     );
816
817     my $item = $builder->build_sample_item;
818
819     # biblio-level hold
820     my $hold_1 = $builder->build_object(
821         {
822             class => 'Koha::Holds',
823             value => {
824                 itemnumber     => undef,
825                 biblionumber   => $item->biblionumber,
826                 borrowernumber => $patron->borrowernumber
827             }
828         }
829     );
830     # item-level hold
831     my $hold_2 = $builder->build_object(
832         {
833             class => 'Koha::Holds',
834             value => {
835                 itemnumber     => $item->itemnumber,
836                 biblionumber   => $item->biblionumber,
837                 borrowernumber => $patron->borrowernumber
838             }
839         }
840     );
841
842     $t->get_ok( "//$userid:$password@/api/v1/holds/"
843           . $hold_1->id
844           . "/pickup_locations" )
845       ->json_is( [ $library_2_api, $library_3_api ] );
846
847     $t->get_ok( "//$userid:$password@/api/v1/holds/"
848           . $hold_2->id
849           . "/pickup_locations" )
850       ->json_is( [ $library_1_api, $library_2_api ] );
851
852     # filtering works!
853     $t->get_ok( "//$userid:$password@/api/v1/holds/"
854           . $hold_2->id
855           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
856       ->json_is( [ $library_1_api ] );
857
858     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
859
860     my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } });
861     my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } });
862
863     my $library_5_api = $library_5->to_api();
864     $library_5_api->{needs_override} = Mojo::JSON->true;
865
866     # bibli-level mock doesn't include library_1 as valid pickup location
867     $library_1_api->{needs_override} = Mojo::JSON->true;
868
869     $t->get_ok( "//$userid:$password@/api/v1/holds/"
870           . $hold_1->id
871           . "/pickup_locations?_order_by=marc_org_code" )
872       ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] );
873
874     $schema->storage->txn_rollback;
875 };
876
877 subtest 'edit() tests' => sub {
878
879     plan tests => 14;
880
881     $schema->storage->txn_begin;
882
883     my $password = 'AbcdEFG123';
884
885     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
886     my $patron = $builder->build_object(
887         { class => 'Koha::Patrons', value => { flags => 1 } } );
888     $patron->set_password( { password => $password, skip_validation => 1 } );
889     my $userid = $patron->userid;
890     $builder->build(
891         {
892             source => 'UserPermission',
893             value  => {
894                 borrowernumber => $patron->borrowernumber,
895                 module_bit     => 6,
896                 code           => 'modify_holds_priority',
897             },
898         }
899     );
900
901     # Disable logging
902     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
903     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
904
905     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
906     my $mock_item   = Test::MockModule->new('Koha::Item');
907
908     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
909     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
910     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
911
912     # let's control what Koha::Biblio->pickup_locations returns, for testing
913     $mock_biblio->mock( 'pickup_locations', sub {
914         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
915     });
916     # let's mock what Koha::Item->pickup_locations returns, for testing
917     $mock_item->mock( 'pickup_locations', sub {
918         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
919     });
920
921     my $biblio = $builder->build_sample_biblio;
922     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
923
924     # Test biblio-level holds
925     my $biblio_hold = $builder->build_object(
926         {
927             class => "Koha::Holds",
928             value => {
929                 biblionumber => $biblio->biblionumber,
930                 branchcode   => $library_3->branchcode,
931                 itemnumber   => undef,
932                 priority     => 1,
933             }
934         }
935     );
936
937     my $biblio_hold_data = $biblio_hold->to_api;
938     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
939
940     $t->put_ok( "//$userid:$password@/api/v1/holds/"
941           . $biblio_hold->id
942           => json => $biblio_hold_data )
943       ->status_is(400)
944       ->json_is({ error => 'The supplied pickup location is not valid' });
945
946     $biblio_hold->discard_changes;
947     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
948
949     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
950     $t->put_ok( "//$userid:$password@/api/v1/holds/"
951           . $biblio_hold->id
952           => json => $biblio_hold_data )
953       ->status_is(200);
954
955     $biblio_hold->discard_changes;
956     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
957
958     # Test item-level holds
959     my $item_hold = $builder->build_object(
960         {
961             class => "Koha::Holds",
962             value => {
963                 biblionumber => $biblio->biblionumber,
964                 branchcode   => $library_3->branchcode,
965                 itemnumber   => $item->itemnumber,
966                 priority     => 1,
967             }
968         }
969     );
970
971     my $item_hold_data = $item_hold->to_api;
972     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
973
974     $t->put_ok( "//$userid:$password@/api/v1/holds/"
975           . $item_hold->id
976           => json => $item_hold_data )
977       ->status_is(400)
978       ->json_is({ error => 'The supplied pickup location is not valid' });
979
980     $item_hold->discard_changes;
981     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
982
983     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
984     $t->put_ok( "//$userid:$password@/api/v1/holds/"
985           . $item_hold->id
986           => json => $item_hold_data )
987       ->status_is(200);
988
989     $item_hold->discard_changes;
990     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
991
992     $schema->storage->txn_rollback;
993 };
994
995 subtest 'add() tests' => sub {
996
997     plan tests => 10;
998
999     $schema->storage->txn_begin;
1000
1001     my $password = 'AbcdEFG123';
1002
1003     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
1004     my $patron = $builder->build_object(
1005         { class => 'Koha::Patrons', value => { flags => 1 } } );
1006     $patron->set_password( { password => $password, skip_validation => 1 } );
1007     my $userid = $patron->userid;
1008     $builder->build(
1009         {
1010             source => 'UserPermission',
1011             value  => {
1012                 borrowernumber => $patron->borrowernumber,
1013                 module_bit     => 6,
1014                 code           => 'modify_holds_priority',
1015             },
1016         }
1017     );
1018
1019     # Disable logging
1020     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1021     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1022
1023     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
1024     my $mock_item   = Test::MockModule->new('Koha::Item');
1025
1026     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1027     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1028     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1029
1030     # let's control what Koha::Biblio->pickup_locations returns, for testing
1031     $mock_biblio->mock( 'pickup_locations', sub {
1032         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1033     });
1034     # let's mock what Koha::Item->pickup_locations returns, for testing
1035     $mock_item->mock( 'pickup_locations', sub {
1036         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1037     });
1038
1039     my $can_be_reserved = 'OK';
1040     my $mock_reserves = Test::MockModule->new('C4::Reserves');
1041     $mock_reserves->mock( 'CanItemBeReserved', sub
1042         {
1043             return { status => $can_be_reserved }
1044         }
1045
1046     );
1047     $mock_reserves->mock( 'CanBookBeReserved', sub
1048         {
1049             return { status => $can_be_reserved }
1050         }
1051
1052     );
1053
1054     my $biblio = $builder->build_sample_biblio;
1055     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1056
1057     # Test biblio-level holds
1058     my $biblio_hold = $builder->build_object(
1059         {
1060             class => "Koha::Holds",
1061             value => {
1062                 biblionumber => $biblio->biblionumber,
1063                 branchcode   => $library_3->branchcode,
1064                 itemnumber   => undef,
1065                 priority     => 1,
1066             }
1067         }
1068     );
1069
1070     my $biblio_hold_data = $biblio_hold->to_api;
1071     $biblio_hold->delete;
1072     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
1073     delete $biblio_hold_data->{hold_id};
1074
1075     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1076       ->status_is(400)
1077       ->json_is({ error => 'The supplied pickup location is not valid' });
1078
1079     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1080     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1081       ->status_is(201);
1082
1083     # Test item-level holds
1084     my $item_hold = $builder->build_object(
1085         {
1086             class => "Koha::Holds",
1087             value => {
1088                 biblionumber => $biblio->biblionumber,
1089                 branchcode   => $library_3->branchcode,
1090                 itemnumber   => $item->itemnumber,
1091                 priority     => 1,
1092             }
1093         }
1094     );
1095
1096     my $item_hold_data = $item_hold->to_api;
1097     $item_hold->delete;
1098     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
1099     delete $item_hold->{hold_id};
1100
1101     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1102       ->status_is(400)
1103       ->json_is({ error => 'The supplied pickup location is not valid' });
1104
1105     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1106     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1107       ->status_is(201);
1108
1109     $schema->storage->txn_rollback;
1110 };
1111
1112 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1113
1114     plan tests => 16;
1115
1116     $schema->storage->txn_begin;
1117
1118     my $password = 'AbcdEFG123';
1119
1120     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1121     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1122     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1123
1124     my $patron = $builder->build_object(
1125         { class => 'Koha::Patrons', value => { flags => 0 } } );
1126     $patron->set_password( { password => $password, skip_validation => 1 } );
1127     my $userid = $patron->userid;
1128     $builder->build(
1129         {
1130             source => 'UserPermission',
1131             value  => {
1132                 borrowernumber => $patron->borrowernumber,
1133                 module_bit     => 6,
1134                 code           => 'place_holds',
1135             },
1136         }
1137     );
1138
1139     # Disable logging
1140     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1141     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1142
1143     my $libraries_query = { branchcode => [ $library_1->branchcode, $library_2->branchcode ] };
1144
1145     my $mocked_biblio = Test::MockModule->new('Koha::Biblio');
1146     $mocked_biblio->mock( 'pickup_locations', sub {
1147         return Koha::Libraries->search($libraries_query);
1148     });
1149
1150     my $mocked_item = Test::MockModule->new('Koha::Item');
1151     $mocked_item->mock( 'pickup_locations', sub {
1152         return Koha::Libraries->search($libraries_query);
1153     });
1154
1155     my $biblio = $builder->build_sample_biblio;
1156     my $item   = $builder->build_sample_item(
1157         {
1158             biblionumber => $biblio->biblionumber,
1159             library      => $library_1->branchcode
1160         }
1161     );
1162
1163     # biblio-level hold
1164     my $hold = Koha::Holds->find(
1165         AddReserve(
1166             {
1167                 branchcode     => $library_1->branchcode,
1168                 borrowernumber => $patron->borrowernumber,
1169                 biblionumber   => $biblio->biblionumber,
1170                 priority       => 1,
1171                 itemnumber     => undef,
1172             }
1173         )
1174     );
1175
1176     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1177           . $hold->id
1178           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1179       ->status_is(200)
1180       ->json_is({ pickup_library_id => $library_2->branchcode });
1181
1182     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1183
1184     $libraries_query = { branchcode => $library_1->branchcode };
1185
1186     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1187           . $hold->id
1188           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1189       ->status_is(400)
1190       ->json_is({ error => '[The supplied pickup location is not valid]' });
1191
1192     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1193
1194     # item-level hold
1195     $hold = Koha::Holds->find(
1196         AddReserve(
1197             {
1198                 branchcode     => $library_1->branchcode,
1199                 borrowernumber => $patron->borrowernumber,
1200                 biblionumber   => $biblio->biblionumber,
1201                 priority       => 1,
1202                 itemnumber     => $item->itemnumber,
1203             }
1204         )
1205     );
1206
1207     $libraries_query = { branchcode => $library_1->branchcode };
1208
1209     # Attempt to use an invalid pickup locations ends in 400
1210     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1211           . $hold->id
1212           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1213       ->status_is(400)
1214       ->json_is({ error => '[The supplied pickup location is not valid]' });
1215
1216     is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' );
1217
1218     $libraries_query = {
1219         branchcode => {
1220             '-in' => [ $library_1->branchcode, $library_2->branchcode ]
1221         }
1222     };
1223
1224     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1225           . $hold->id
1226           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1227       ->status_is(200)
1228       ->json_is({ pickup_library_id => $library_2->branchcode });
1229
1230     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1231
1232     $schema->storage->txn_rollback;
1233 };