585e19a8a506bfdf67b69ba22a5ae7acaf8f9c29
[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 => 10;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::TestBuilder;
24 use t::lib::Mocks;
25
26 use DateTime;
27
28 use C4::Context;
29 use Koha::Patrons;
30 use C4::Reserves;
31 use C4::Items;
32
33 use Koha::Database;
34 use Koha::DateUtils;
35 use Koha::Biblios;
36 use Koha::Biblioitems;
37 use Koha::Items;
38 use Koha::CirculationRules;
39
40 my $schema  = Koha::Database->new->schema;
41 my $builder = t::lib::TestBuilder->new();
42
43 $schema->storage->txn_begin;
44
45 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
46
47 my $t = Test::Mojo->new('Koha::REST::V1');
48
49 my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
50 my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
51 my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
52 my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype};
53
54 # Generic password for everyone
55 my $password = 'thePassword123';
56
57 # User without any permissions
58 my $nopermission = $builder->build_object({
59     class => 'Koha::Patrons',
60     value => {
61         branchcode   => $branchcode,
62         categorycode => $categorycode,
63         flags        => 0
64     }
65 });
66 $nopermission->set_password( { password => $password, skip_validation => 1 } );
67 my $nopermission_userid = $nopermission->userid;
68
69 my $patron_1 = $builder->build_object(
70     {
71         class => 'Koha::Patrons',
72         value => {
73             categorycode => $categorycode,
74             branchcode   => $branchcode,
75             surname      => 'Test Surname',
76             flags        => 80, #borrowers and reserveforothers flags
77         }
78     }
79 );
80 $patron_1->set_password( { password => $password, skip_validation => 1 } );
81 my $userid_1 = $patron_1->userid;
82
83 my $patron_2 = $builder->build_object(
84     {
85         class => 'Koha::Patrons',
86         value => {
87             categorycode => $categorycode,
88             branchcode   => $branchcode,
89             surname      => 'Test Surname 2',
90             flags        => 16, # borrowers flag
91         }
92     }
93 );
94 $patron_2->set_password( { password => $password, skip_validation => 1 } );
95 my $userid_2 = $patron_2->userid;
96
97 my $patron_3 = $builder->build_object(
98     {
99         class => 'Koha::Patrons',
100         value => {
101             categorycode => $categorycode,
102             branchcode   => $branchcode,
103             surname      => 'Test Surname 3',
104             flags        => 64, # reserveforothers flag
105         }
106     }
107 );
108 $patron_3->set_password( { password => $password, skip_validation => 1 } );
109 my $userid_3 = $patron_3->userid;
110
111 my $biblio_1 = $builder->build_sample_biblio;
112 my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber, itype => $itemtype });
113
114 my $biblio_2 = $builder->build_sample_biblio;
115 my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber, itype => $itemtype });
116
117 my $dbh = C4::Context->dbh;
118 $dbh->do('DELETE FROM reserves');
119 Koha::CirculationRules->search()->delete();
120 Koha::CirculationRules->set_rules(
121     {
122         categorycode => undef,
123         branchcode   => undef,
124         itemtype     => undef,
125         rules        => {
126             reservesallowed => 1,
127             holds_per_record => 99
128         }
129     }
130 );
131
132 my $reserve_id = C4::Reserves::AddReserve(
133     {
134         branchcode     => $branchcode,
135         borrowernumber => $patron_1->borrowernumber,
136         biblionumber   => $biblio_1->biblionumber,
137         priority       => 1,
138         itemnumber     => $item_1->itemnumber,
139     }
140 );
141
142 # Add another reserve to be able to change first reserve's rank
143 my $reserve_id2 = C4::Reserves::AddReserve(
144     {
145         branchcode     => $branchcode,
146         borrowernumber => $patron_2->borrowernumber,
147         biblionumber   => $biblio_1->biblionumber,
148         priority       => 2,
149         itemnumber     => $item_1->itemnumber,
150     }
151 );
152
153 my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' );
154 my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
155
156 my $post_data = {
157     patron_id => int($patron_1->borrowernumber),
158     biblio_id => int($biblio_1->biblionumber),
159     item_id => int($item_1->itemnumber),
160     pickup_library_id => $branchcode,
161     expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
162     priority => 2,
163 };
164 my $put_data = {
165     priority => 2,
166     suspended_until => output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }),
167 };
168
169 subtest "Test endpoints without authentication" => sub {
170     plan tests => 8;
171     $t->get_ok('/api/v1/holds')
172       ->status_is(401);
173     $t->post_ok('/api/v1/holds')
174       ->status_is(401);
175     $t->put_ok('/api/v1/holds/0')
176       ->status_is(401);
177     $t->delete_ok('/api/v1/holds/0')
178       ->status_is(401);
179 };
180
181 subtest "Test endpoints without permission" => sub {
182
183     plan tests => 10;
184
185     $t->get_ok( "//$nopermission_userid:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber ) # no permission
186       ->status_is(403);
187
188     $t->get_ok( "//$userid_3:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )    # no permission
189       ->status_is(403);
190
191     $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data )
192       ->status_is(403);
193
194     $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_data )
195       ->status_is(403);
196
197     $t->delete_ok( "//$nopermission_userid:$password@/api/v1/holds/0" )
198       ->status_is(403);
199 };
200
201 subtest "Test endpoints with permission" => sub {
202
203     plan tests => 62;
204
205     $t->get_ok( "//$userid_1:$password@/api/v1/holds" )
206       ->status_is(200)
207       ->json_has('/0')
208       ->json_has('/1')
209       ->json_hasnt('/2');
210
211     $t->get_ok( "//$userid_1:$password@/api/v1/holds?priority=2" )
212       ->status_is(200)
213       ->json_is('/0/patron_id', $patron_2->borrowernumber)
214       ->json_hasnt('/1');
215
216     # While suspended_until is date-time, it's always set to midnight.
217     my $expected_suspended_until = $suspended_until->strftime('%FT00:00:00%z');
218     substr($expected_suspended_until, -2, 0, ':');
219
220     $t->put_ok( "//$userid_1:$password@/api/v1/holds/$reserve_id" => json => $put_data )
221       ->status_is(200)
222       ->json_is( '/hold_id', $reserve_id )
223       ->json_is( '/suspended_until', $expected_suspended_until )
224       ->json_is( '/priority', 2 )
225       ->json_is( '/pickup_library_id', $branchcode );
226
227     # Change only pickup library, everything else should remain
228     $t->put_ok( "//$userid_1:$password@/api/v1/holds/$reserve_id" => json => { pickup_library_id => $branchcode2 } )
229       ->status_is(200)
230       ->json_is( '/hold_id', $reserve_id )
231       ->json_is( '/suspended_until', $expected_suspended_until )
232       ->json_is( '/priority', 2 )
233       ->json_is( '/pickup_library_id', $branchcode2 );
234
235     # Reset suspended_until, everything else should remain
236     $t->put_ok( "//$userid_1:$password@/api/v1/holds/$reserve_id" => json => { suspended_until => undef } )
237       ->status_is(200)
238       ->json_is( '/hold_id', $reserve_id )
239       ->json_is( '/suspended_until', undef )
240       ->json_is( '/priority', 2 )
241       ->json_is( '/pickup_library_id', $branchcode2 );
242
243     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
244       ->status_is(204, 'SWAGGER3.2.4')
245       ->content_is('', 'SWAGGER3.3.4');
246
247     $t->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data )
248       ->status_is(404)
249       ->json_has('/error');
250
251     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
252       ->status_is(404)
253       ->json_has('/error');
254
255     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
256       ->status_is(200)
257       ->json_is([]);
258
259     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
260     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
261       ->status_is(200)
262       ->json_is([]);
263
264     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
265       ->status_is(204, 'SWAGGER3.2.4')
266       ->content_is('', 'SWAGGER3.3.4');
267
268     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
269       ->status_is(201)
270       ->json_has('/hold_id');
271     # Get id from response
272     $reserve_id = $t->tx->res->json->{hold_id};
273
274     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
275       ->status_is(200)
276       ->json_is('/0/hold_id', $reserve_id)
277       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
278       ->json_is('/0/pickup_library_id', $branchcode);
279
280     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
281       ->status_is(403)
282       ->json_like('/error', qr/itemAlreadyOnHold/);
283
284     $post_data->{biblionumber} = int($biblio_2->biblionumber);
285     $post_data->{itemnumber}   = int($item_2->itemnumber);
286
287     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
288       ->status_is(403)
289       ->json_like('/error', qr/itemAlreadyOnHold/);
290
291     my $to_delete_patron  = $builder->build_object({ class => 'Koha::Patrons' });
292     my $deleted_patron_id = $to_delete_patron->borrowernumber;
293     $to_delete_patron->delete;
294
295     my $tmp_patron_id = $post_data->{patron_id};
296     $post_data->{patron_id} = $deleted_patron_id;
297     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
298       ->status_is(400)
299       ->json_is( { error => 'patron_id not found' } );
300
301     # Restore the original patron_id as it is expected by the next subtest
302     # FIXME: this tests need to be rewritten from scratch
303     $post_data->{patron_id} = $tmp_patron_id;
304 };
305
306 subtest 'Reserves with itemtype' => sub {
307     plan tests => 10;
308
309     my $post_data = {
310         patron_id => int($patron_1->borrowernumber),
311         biblio_id => int($biblio_1->biblionumber),
312         pickup_library_id => $branchcode,
313         item_type => $itemtype,
314     };
315
316     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
317       ->status_is(204, 'SWAGGER3.2.4')
318       ->content_is('', 'SWAGGER3.3.4');
319
320     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
321       ->status_is(201)
322       ->json_has('/hold_id');
323
324     $reserve_id = $t->tx->res->json->{hold_id};
325
326     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
327       ->status_is(200)
328       ->json_is('/0/hold_id', $reserve_id)
329       ->json_is('/0/item_type', $itemtype);
330 };
331
332
333 subtest 'test AllowHoldDateInFuture' => sub {
334
335     plan tests => 6;
336
337     $dbh->do('DELETE FROM reserves');
338
339     my $future_hold_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
340
341     my $post_data = {
342         patron_id => int($patron_1->borrowernumber),
343         biblio_id => int($biblio_1->biblionumber),
344         item_id => int($item_1->itemnumber),
345         pickup_library_id => $branchcode,
346         expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
347         hold_date => output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }),
348         priority => 2,
349     };
350
351     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 );
352
353     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
354       ->status_is(400)
355       ->json_has('/error');
356
357     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 );
358
359     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
360       ->status_is(201)
361       ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }));
362 };
363
364 subtest 'test AllowHoldPolicyOverride' => sub {
365
366     plan tests => 5;
367
368     $dbh->do('DELETE FROM reserves');
369
370     Koha::CirculationRules->set_rules(
371         {
372             itemtype     => undef,
373             branchcode   => undef,
374             rules        => {
375                 holdallowed              => 1
376             }
377         }
378     );
379
380     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
381
382     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
383       ->status_is(403)
384       ->json_has('/error');
385
386     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
387
388     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
389       ->status_is(201);
390 };
391
392 $schema->storage->txn_rollback;
393
394 subtest 'suspend and resume tests' => sub {
395
396     plan tests => 24;
397
398     $schema->storage->txn_begin;
399
400     my $password = 'AbcdEFG123';
401
402     my $patron = $builder->build_object(
403         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
404     $patron->set_password({ password => $password, skip_validation => 1 });
405     my $userid = $patron->userid;
406
407     # Disable logging
408     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
409     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
410
411     my $hold = $builder->build_object(
412         {   class => 'Koha::Holds',
413             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
414         }
415     );
416
417     ok( !$hold->is_suspended, 'Hold is not suspended' );
418     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
419         ->status_is( 201, 'Hold suspension created' );
420
421     $hold->discard_changes;    # refresh object
422
423     ok( $hold->is_suspended, 'Hold is suspended' );
424     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
425
426     my $end_date = output_pref({
427       dt         => dt_from_string( undef ),
428       dateformat => 'rfc3339',
429       dateonly   => 1
430     });
431
432     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
433
434     $hold->discard_changes;    # refresh object
435
436     ok( $hold->is_suspended, 'Hold is suspended' );
437     $t->json_is(
438       '/end_date',
439       output_pref({
440         dt         => dt_from_string( $hold->suspend_until ),
441         dateformat => 'rfc3339',
442         dateonly   => 1
443       }),
444       'Hold suspension has correct end date'
445     );
446
447     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
448       ->status_is(204, 'SWAGGER3.2.4')
449       ->content_is('', 'SWAGGER3.3.4');
450
451     # Pass a an expiration date for the suspension
452     my $date = dt_from_string()->add( days => 5 );
453     $t->post_ok(
454               "//$userid:$password@/api/v1/holds/"
455             . $hold->id
456             . "/suspension" => json => {
457             end_date =>
458                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
459             }
460     )->status_is( 201, 'Hold suspension created' )
461         ->json_is( '/end_date',
462         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
463         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
464
465     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
466       ->status_is(204, 'SWAGGER3.2.4')
467       ->content_is('', 'SWAGGER3.3.4');
468
469     $hold->set_waiting->discard_changes;
470
471     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
472       ->status_is( 400, 'Cannot suspend waiting hold' )
473       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
474
475     $hold->set_transfer->discard_changes;
476
477     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
478       ->status_is( 400, 'Cannot suspend hold on transfer' )
479       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
480
481     $schema->storage->txn_rollback;
482 };
483
484 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
485
486     plan tests => 14;
487
488     $schema->storage->txn_begin;
489
490     my $password = 'AbcdEFG123';
491
492     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
493     my $patron_np = $builder->build_object(
494         { class => 'Koha::Patrons', value => { flags => 0 } } );
495     $patron_np->set_password( { password => $password, skip_validation => 1 } );
496     my $userid_np = $patron_np->userid;
497
498     my $patron = $builder->build_object(
499         { class => 'Koha::Patrons', value => { flags => 0 } } );
500     $patron->set_password( { password => $password, skip_validation => 1 } );
501     my $userid = $patron->userid;
502     $builder->build(
503         {
504             source => 'UserPermission',
505             value  => {
506                 borrowernumber => $patron->borrowernumber,
507                 module_bit     => 6,
508                 code           => 'modify_holds_priority',
509             },
510         }
511     );
512
513     # Disable logging
514     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
515     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
516
517     my $biblio   = $builder->build_sample_biblio;
518     my $patron_1 = $builder->build_object(
519         {
520             class => 'Koha::Patrons',
521             value => { branchcode => $library->branchcode }
522         }
523     );
524     my $patron_2 = $builder->build_object(
525         {
526             class => 'Koha::Patrons',
527             value => { branchcode => $library->branchcode }
528         }
529     );
530     my $patron_3 = $builder->build_object(
531         {
532             class => 'Koha::Patrons',
533             value => { branchcode => $library->branchcode }
534         }
535     );
536
537     my $hold_1 = Koha::Holds->find(
538         AddReserve(
539             {
540                 branchcode     => $library->branchcode,
541                 borrowernumber => $patron_1->borrowernumber,
542                 biblionumber   => $biblio->biblionumber,
543                 priority       => 1,
544             }
545         )
546     );
547     my $hold_2 = Koha::Holds->find(
548         AddReserve(
549             {
550                 branchcode     => $library->branchcode,
551                 borrowernumber => $patron_2->borrowernumber,
552                 biblionumber   => $biblio->biblionumber,
553                 priority       => 2,
554             }
555         )
556     );
557     my $hold_3 = Koha::Holds->find(
558         AddReserve(
559             {
560                 branchcode     => $library->branchcode,
561                 borrowernumber => $patron_3->borrowernumber,
562                 biblionumber   => $biblio->biblionumber,
563                 priority       => 3,
564             }
565         )
566     );
567
568     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
569           . $hold_3->id
570           . "/priority" => json => 1 )->status_is(403);
571
572     $t->put_ok( "//$userid:$password@/api/v1/holds/"
573           . $hold_3->id
574           . "/priority" => json => 1 )->status_is(200)->json_is(1);
575
576     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
577     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
578     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
579
580     $t->put_ok( "//$userid:$password@/api/v1/holds/"
581           . $hold_3->id
582           . "/priority" => json => 3 )->status_is(200)->json_is(3);
583
584     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
585     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
586     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
587
588     $schema->storage->txn_rollback;
589 };
590
591 subtest 'add() tests (maxreserves behaviour)' => sub {
592
593     plan tests => 7;
594
595     $schema->storage->txn_begin;
596
597     $dbh->do('DELETE FROM reserves');
598
599     Koha::CirculationRules->new->delete;
600
601     my $password = 'AbcdEFG123';
602
603     my $patron = $builder->build_object(
604         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
605     $patron->set_password({ password => $password, skip_validation => 1 });
606     my $userid = $patron->userid;
607
608     Koha::CirculationRules->set_rules(
609         {
610             itemtype     => undef,
611             branchcode   => undef,
612             categorycode => undef,
613             rules        => {
614                 reservesallowed => 3
615             }
616         }
617     );
618
619     Koha::CirculationRules->set_rules(
620         {
621             branchcode   => undef,
622             categorycode => $patron->categorycode,
623             rules        => {
624                 max_holds   => 4,
625             }
626         }
627     );
628
629     my $biblio_1 = $builder->build_sample_biblio;
630     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
631     my $biblio_2 = $builder->build_sample_biblio;
632     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
633     my $biblio_3 = $builder->build_sample_biblio;
634     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
635
636     # Disable logging
637     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
638     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
639     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
640     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
641
642     my $post_data = {
643         patron_id => $patron->borrowernumber,
644         biblio_id => $biblio_1->biblionumber,
645         pickup_library_id => $item_1->home_branch->branchcode,
646         item_type => $item_1->itype,
647     };
648
649     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
650       ->status_is(201);
651
652     $post_data = {
653         patron_id => $patron->borrowernumber,
654         biblio_id => $biblio_2->biblionumber,
655         pickup_library_id => $item_2->home_branch->branchcode,
656         item_id   => $item_2->itemnumber
657     };
658
659     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
660       ->status_is(201);
661
662     $post_data = {
663         patron_id => $patron->borrowernumber,
664         biblio_id => $biblio_3->biblionumber,
665         pickup_library_id => $item_1->home_branch->branchcode,
666         item_id   => $item_3->itemnumber
667     };
668
669     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
670       ->status_is(403)
671       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
672
673     $schema->storage->txn_rollback;
674 };
675
676 subtest 'pickup_locations() tests' => sub {
677
678     plan tests => 6;
679
680     $schema->storage->txn_begin;
681
682     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A' } });
683     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B' } });
684     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C' } });
685
686     my $patron = $builder->build_object(
687         {
688             class => 'Koha::Patrons',
689             value => { userid => 'tomasito', flags => 1 }
690         }
691     );
692     $patron->set_password( { password => $password, skip_validation => 1 } );
693     my $userid = $patron->userid;
694
695     my $item_class = Test::MockModule->new('Koha::Item');
696     $item_class->mock(
697         'pickup_locations',
698         sub {
699             my ( $self, $params ) = @_;
700             my $mock_patron = $params->{patron};
701             is( $mock_patron->borrowernumber,
702                 $patron->borrowernumber, 'Patron passed correctly' );
703             return Koha::Libraries->search(
704                 {
705                     branchcode => {
706                         '-in' => [
707                             $library_1->branchcode,
708                             $library_2->branchcode
709                         ]
710                     }
711                 },
712                 {   # we make sure no surprises in the order of the result
713                     order_by => { '-asc' => 'marcorgcode' }
714                 }
715             );
716         }
717     );
718
719     my $biblio_class = Test::MockModule->new('Koha::Biblio');
720     $biblio_class->mock(
721         'pickup_locations',
722         sub {
723             my ( $self, $params ) = @_;
724             my $mock_patron = $params->{patron};
725             is( $mock_patron->borrowernumber,
726                 $patron->borrowernumber, 'Patron passed correctly' );
727             return Koha::Libraries->search(
728                 {
729                     branchcode => {
730                         '-in' => [
731                             $library_2->branchcode,
732                             $library_3->branchcode
733                         ]
734                     }
735                 },
736                 {   # we make sure no surprises in the order of the result
737                     order_by => { '-asc' => 'marcorgcode' }
738                 }
739             );
740         }
741     );
742
743     my $item = $builder->build_sample_item;
744
745     # biblio-level hold
746     my $hold_1 = $builder->build_object(
747         {
748             class => 'Koha::Holds',
749             value => {
750                 itemnumber     => undef,
751                 biblionumber   => $item->biblionumber,
752                 borrowernumber => $patron->borrowernumber
753             }
754         }
755     );
756     # item-level hold
757     my $hold_2 = $builder->build_object(
758         {
759             class => 'Koha::Holds',
760             value => {
761                 itemnumber     => $item->itemnumber,
762                 biblionumber   => $item->biblionumber,
763                 borrowernumber => $patron->borrowernumber
764             }
765         }
766     );
767
768     $t->get_ok( "//$userid:$password@/api/v1/holds/"
769           . $hold_1->id
770           . "/pickup_locations" )
771       ->json_is( [ $library_2->to_api, $library_3->to_api ] );
772
773     $t->get_ok( "//$userid:$password@/api/v1/holds/"
774           . $hold_2->id
775           . "/pickup_locations" )
776       ->json_is( [ $library_1->to_api, $library_2->to_api ] );
777
778     $schema->storage->txn_rollback;
779 };