Bug 28369: Regression tests
[koha.git] / 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         => $patron_1->borrowernumber,
159     biblio_id         => $biblio_1->biblionumber,
160     item_id           => $item_1->itemnumber,
161     pickup_library_id => $branchcode,
162     expiration_date   => output_pref( { dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 } ),
163 };
164 my $patch_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->patch_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->patch_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $patch_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 => 44;
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     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
217       ->status_is(204, 'SWAGGER3.2.4')
218       ->content_is('', 'SWAGGER3.3.4');
219
220     $t->patch_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $patch_data )
221       ->status_is(404)
222       ->json_has('/error');
223
224     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
225       ->status_is(404)
226       ->json_has('/error');
227
228     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
229       ->status_is(200)
230       ->json_is([]);
231
232     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
233     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
234       ->status_is(200)
235       ->json_is([]);
236
237     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
238       ->status_is(204, 'SWAGGER3.2.4')
239       ->content_is('', 'SWAGGER3.3.4');
240
241     # Make sure pickup location checks doesn't get in the middle
242     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
243     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
244     my $mock_item   = Test::MockModule->new('Koha::Item');
245     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
246
247     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
248       ->status_is(201)
249       ->json_has('/hold_id');
250
251     # Get id from response
252     $reserve_id = $t->tx->res->json->{hold_id};
253
254     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
255       ->status_is(200)
256       ->json_is('/0/hold_id', $reserve_id)
257       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
258       ->json_is('/0/pickup_library_id', $branchcode);
259
260     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
261       ->status_is(403)
262       ->json_like('/error', qr/itemAlreadyOnHold/);
263
264     $post_data->{biblio_id} = $biblio_2->biblionumber;
265     $post_data->{item_id}   = $item_2->itemnumber;
266
267     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
268       ->status_is(403)
269       ->json_like('/error', qr/Hold cannot be placed. Reason: tooManyReserves/);
270
271     my $to_delete_patron  = $builder->build_object({ class => 'Koha::Patrons' });
272     my $deleted_patron_id = $to_delete_patron->borrowernumber;
273     $to_delete_patron->delete;
274
275     my $tmp_patron_id = $post_data->{patron_id};
276     $post_data->{patron_id} = $deleted_patron_id;
277     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
278       ->status_is(400)
279       ->json_is( { error => 'patron_id not found' } );
280
281     # Restore the original patron_id as it is expected by the next subtest
282     # FIXME: this tests need to be rewritten from scratch
283     $post_data->{patron_id} = $tmp_patron_id;
284 };
285
286 subtest 'Reserves with itemtype' => sub {
287     plan tests => 10;
288
289     my $post_data = {
290         patron_id => int($patron_1->borrowernumber),
291         biblio_id => int($biblio_1->biblionumber),
292         pickup_library_id => $branchcode,
293         item_type => $itemtype,
294     };
295
296     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
297       ->status_is(204, 'SWAGGER3.2.4')
298       ->content_is('', 'SWAGGER3.3.4');
299
300     # Make sure pickup location checks doesn't get in the middle
301     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
302     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
303     my $mock_item   = Test::MockModule->new('Koha::Item');
304     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
305
306     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
307       ->status_is(201)
308       ->json_has('/hold_id');
309
310     $reserve_id = $t->tx->res->json->{hold_id};
311
312     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
313       ->status_is(200)
314       ->json_is('/0/hold_id', $reserve_id)
315       ->json_is('/0/item_type', $itemtype);
316 };
317
318
319 subtest 'test AllowHoldDateInFuture' => sub {
320
321     plan tests => 6;
322
323     $dbh->do('DELETE FROM reserves');
324
325     my $future_hold_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
326
327     my $post_data = {
328         patron_id         => $patron_1->borrowernumber,
329         biblio_id         => $biblio_1->biblionumber,
330         item_id           => $item_1->itemnumber,
331         pickup_library_id => $branchcode,
332         expiration_date   => output_pref( { dt => $expiration_date,  dateformat => 'rfc3339', dateonly => 1 } ),
333         hold_date         => output_pref( { dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 } ),
334     };
335
336     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 );
337
338     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
339       ->status_is(400)
340       ->json_has('/error');
341
342     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 );
343
344     # Make sure pickup location checks doesn't get in the middle
345     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
346     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
347     my $mock_item   = Test::MockModule->new('Koha::Item');
348     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
349
350     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
351       ->status_is(201)
352       ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }));
353 };
354
355 $schema->storage->txn_rollback;
356
357 subtest 'x-koha-override and AllowHoldPolicyOverride tests' => sub {
358
359     plan tests => 18;
360
361     $schema->storage->txn_begin;
362
363     my $patron = $builder->build_object(
364         {
365             class => 'Koha::Patrons',
366             value => { flags => 1 }
367         }
368     );
369     my $password = 'thePassword123';
370     $patron->set_password( { password => $password, skip_validation => 1 } );
371     $patron->discard_changes;
372     my $userid = $patron->userid;
373
374     my $renegade_library = $builder->build_object({ class => 'Koha::Libraries' });
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({ branchcode => { '!=' => $renegade_library->branchcode } }); } );
382     my $mock_item = Test::MockModule->new('Koha::Item');
383     $mock_item->mock( 'pickup_locations',
384         sub { return Koha::Libraries->search({ branchcode => { '!=' => $renegade_library->branchcode } }) } );
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     # Test pickup locations can be overridden
437     $post_data->{pickup_library_id} = $renegade_library->branchcode;
438
439     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
440       ->status_is(400);
441
442     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
443           { 'x-koha-override' => 'any' } => json => $post_data )
444       ->status_is(201);
445
446     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0);
447
448     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
449           { 'x-koha-override' => 'any' } => json => $post_data )
450       ->status_is(400);
451
452     $schema->storage->txn_rollback;
453 };
454
455 subtest 'suspend and resume tests' => sub {
456
457     plan tests => 24;
458
459     $schema->storage->txn_begin;
460
461     my $password = 'AbcdEFG123';
462
463     my $patron = $builder->build_object(
464         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
465     $patron->set_password({ password => $password, skip_validation => 1 });
466     my $userid = $patron->userid;
467
468     # Disable logging
469     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
470     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
471
472     my $hold = $builder->build_object(
473         {   class => 'Koha::Holds',
474             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
475         }
476     );
477
478     ok( !$hold->is_suspended, 'Hold is not suspended' );
479     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
480         ->status_is( 201, 'Hold suspension created' );
481
482     $hold->discard_changes;    # refresh object
483
484     ok( $hold->is_suspended, 'Hold is suspended' );
485     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
486
487     my $end_date = output_pref({
488       dt         => dt_from_string( undef ),
489       dateformat => 'rfc3339',
490       dateonly   => 1
491     });
492
493     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
494
495     $hold->discard_changes;    # refresh object
496
497     ok( $hold->is_suspended, 'Hold is suspended' );
498     $t->json_is(
499       '/end_date',
500       output_pref({
501         dt         => dt_from_string( $hold->suspend_until ),
502         dateformat => 'rfc3339',
503         dateonly   => 1
504       }),
505       'Hold suspension has correct end date'
506     );
507
508     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
509       ->status_is(204, 'SWAGGER3.2.4')
510       ->content_is('', 'SWAGGER3.3.4');
511
512     # Pass a an expiration date for the suspension
513     my $date = dt_from_string()->add( days => 5 );
514     $t->post_ok(
515               "//$userid:$password@/api/v1/holds/"
516             . $hold->id
517             . "/suspension" => json => {
518             end_date =>
519                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
520             }
521     )->status_is( 201, 'Hold suspension created' )
522         ->json_is( '/end_date',
523         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
524         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
525
526     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
527       ->status_is(204, 'SWAGGER3.2.4')
528       ->content_is('', 'SWAGGER3.3.4');
529
530     $hold->set_waiting->discard_changes;
531
532     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
533       ->status_is( 400, 'Cannot suspend waiting hold' )
534       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
535
536     $hold->set_transfer->discard_changes;
537
538     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
539       ->status_is( 400, 'Cannot suspend hold on transfer' )
540       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
541
542     $schema->storage->txn_rollback;
543 };
544
545 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
546
547     plan tests => 14;
548
549     $schema->storage->txn_begin;
550
551     my $password = 'AbcdEFG123';
552
553     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
554     my $patron_np = $builder->build_object(
555         { class => 'Koha::Patrons', value => { flags => 0 } } );
556     $patron_np->set_password( { password => $password, skip_validation => 1 } );
557     my $userid_np = $patron_np->userid;
558
559     my $patron = $builder->build_object(
560         { class => 'Koha::Patrons', value => { flags => 0 } } );
561     $patron->set_password( { password => $password, skip_validation => 1 } );
562     my $userid = $patron->userid;
563     $builder->build(
564         {
565             source => 'UserPermission',
566             value  => {
567                 borrowernumber => $patron->borrowernumber,
568                 module_bit     => 6,
569                 code           => 'modify_holds_priority',
570             },
571         }
572     );
573
574     # Disable logging
575     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
576     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
577
578     my $biblio   = $builder->build_sample_biblio;
579     my $patron_1 = $builder->build_object(
580         {
581             class => 'Koha::Patrons',
582             value => { branchcode => $library->branchcode }
583         }
584     );
585     my $patron_2 = $builder->build_object(
586         {
587             class => 'Koha::Patrons',
588             value => { branchcode => $library->branchcode }
589         }
590     );
591     my $patron_3 = $builder->build_object(
592         {
593             class => 'Koha::Patrons',
594             value => { branchcode => $library->branchcode }
595         }
596     );
597
598     my $hold_1 = Koha::Holds->find(
599         AddReserve(
600             {
601                 branchcode     => $library->branchcode,
602                 borrowernumber => $patron_1->borrowernumber,
603                 biblionumber   => $biblio->biblionumber,
604                 priority       => 1,
605             }
606         )
607     );
608     my $hold_2 = Koha::Holds->find(
609         AddReserve(
610             {
611                 branchcode     => $library->branchcode,
612                 borrowernumber => $patron_2->borrowernumber,
613                 biblionumber   => $biblio->biblionumber,
614                 priority       => 2,
615             }
616         )
617     );
618     my $hold_3 = Koha::Holds->find(
619         AddReserve(
620             {
621                 branchcode     => $library->branchcode,
622                 borrowernumber => $patron_3->borrowernumber,
623                 biblionumber   => $biblio->biblionumber,
624                 priority       => 3,
625             }
626         )
627     );
628
629     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
630           . $hold_3->id
631           . "/priority" => json => 1 )->status_is(403);
632
633     $t->put_ok( "//$userid:$password@/api/v1/holds/"
634           . $hold_3->id
635           . "/priority" => json => 1 )->status_is(200)->json_is(1);
636
637     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
638     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
639     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
640
641     $t->put_ok( "//$userid:$password@/api/v1/holds/"
642           . $hold_3->id
643           . "/priority" => json => 3 )->status_is(200)->json_is(3);
644
645     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
646     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
647     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
648
649     $schema->storage->txn_rollback;
650 };
651
652 subtest 'add() tests (maxreserves behaviour)' => sub {
653
654     plan tests => 7;
655
656     $schema->storage->txn_begin;
657
658     $dbh->do('DELETE FROM reserves');
659
660     Koha::CirculationRules->new->delete;
661
662     my $password = 'AbcdEFG123';
663
664     my $patron = $builder->build_object(
665         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
666     $patron->set_password({ password => $password, skip_validation => 1 });
667     my $userid = $patron->userid;
668
669     Koha::CirculationRules->set_rules(
670         {
671             itemtype     => undef,
672             branchcode   => undef,
673             categorycode => undef,
674             rules        => {
675                 reservesallowed => 3
676             }
677         }
678     );
679
680     Koha::CirculationRules->set_rules(
681         {
682             branchcode   => undef,
683             categorycode => $patron->categorycode,
684             rules        => {
685                 max_holds   => 4,
686             }
687         }
688     );
689
690     my $biblio_1 = $builder->build_sample_biblio;
691     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
692     my $biblio_2 = $builder->build_sample_biblio;
693     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
694     my $biblio_3 = $builder->build_sample_biblio;
695     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
696
697     # Make sure pickup location checks doesn't get in the middle
698     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
699     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
700     my $mock_item   = Test::MockModule->new('Koha::Item');
701     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
702
703     # Disable logging
704     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
705     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
706     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
707     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
708
709     my $post_data = {
710         patron_id => $patron->borrowernumber,
711         biblio_id => $biblio_1->biblionumber,
712         pickup_library_id => $item_1->home_branch->branchcode,
713         item_type => $item_1->itype,
714     };
715
716     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
717       ->status_is(201);
718
719     $post_data = {
720         patron_id => $patron->borrowernumber,
721         biblio_id => $biblio_2->biblionumber,
722         pickup_library_id => $item_2->home_branch->branchcode,
723         item_id   => $item_2->itemnumber
724     };
725
726     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
727       ->status_is(201);
728
729     $post_data = {
730         patron_id => $patron->borrowernumber,
731         biblio_id => $biblio_3->biblionumber,
732         pickup_library_id => $item_1->home_branch->branchcode,
733         item_id   => $item_3->itemnumber
734     };
735
736     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
737       ->status_is(403)
738       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
739
740     $schema->storage->txn_rollback;
741 };
742
743 subtest 'pickup_locations() tests' => sub {
744
745     plan tests => 12;
746
747     $schema->storage->txn_begin;
748
749     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
750
751     # Small trick to ease testing
752     Koha::Libraries->search->update({ pickup_location => 0 });
753
754     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } });
755     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } });
756     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } });
757
758     my $library_1_api = $library_1->to_api();
759     my $library_2_api = $library_2->to_api();
760     my $library_3_api = $library_3->to_api();
761
762     $library_1_api->{needs_override} = Mojo::JSON->false;
763     $library_2_api->{needs_override} = Mojo::JSON->false;
764     $library_3_api->{needs_override} = Mojo::JSON->false;
765
766     my $patron = $builder->build_object(
767         {
768             class => 'Koha::Patrons',
769             value => { userid => 'tomasito', flags => 0 }
770         }
771     );
772     $patron->set_password( { password => $password, skip_validation => 1 } );
773     my $userid = $patron->userid;
774     $builder->build(
775         {
776             source => 'UserPermission',
777             value  => {
778                 borrowernumber => $patron->borrowernumber,
779                 module_bit     => 6,
780                 code           => 'place_holds',
781             },
782         }
783     );
784
785     my $item_class = Test::MockModule->new('Koha::Item');
786     $item_class->mock(
787         'pickup_locations',
788         sub {
789             my ( $self, $params ) = @_;
790             my $mock_patron = $params->{patron};
791             is( $mock_patron->borrowernumber,
792                 $patron->borrowernumber, 'Patron passed correctly' );
793             return Koha::Libraries->search(
794                 {
795                     branchcode => {
796                         '-in' => [
797                             $library_1->branchcode,
798                             $library_2->branchcode
799                         ]
800                     }
801                 },
802                 {   # we make sure no surprises in the order of the result
803                     order_by => { '-asc' => 'marcorgcode' }
804                 }
805             );
806         }
807     );
808
809     my $biblio_class = Test::MockModule->new('Koha::Biblio');
810     $biblio_class->mock(
811         'pickup_locations',
812         sub {
813             my ( $self, $params ) = @_;
814             my $mock_patron = $params->{patron};
815             is( $mock_patron->borrowernumber,
816                 $patron->borrowernumber, 'Patron passed correctly' );
817             return Koha::Libraries->search(
818                 {
819                     branchcode => {
820                         '-in' => [
821                             $library_2->branchcode,
822                             $library_3->branchcode
823                         ]
824                     }
825                 },
826                 {   # we make sure no surprises in the order of the result
827                     order_by => { '-asc' => 'marcorgcode' }
828                 }
829             );
830         }
831     );
832
833     my $item = $builder->build_sample_item;
834
835     # biblio-level hold
836     my $hold_1 = $builder->build_object(
837         {
838             class => 'Koha::Holds',
839             value => {
840                 itemnumber     => undef,
841                 biblionumber   => $item->biblionumber,
842                 borrowernumber => $patron->borrowernumber
843             }
844         }
845     );
846     # item-level hold
847     my $hold_2 = $builder->build_object(
848         {
849             class => 'Koha::Holds',
850             value => {
851                 itemnumber     => $item->itemnumber,
852                 biblionumber   => $item->biblionumber,
853                 borrowernumber => $patron->borrowernumber
854             }
855         }
856     );
857
858     $t->get_ok( "//$userid:$password@/api/v1/holds/"
859           . $hold_1->id
860           . "/pickup_locations" )
861       ->json_is( [ $library_2_api, $library_3_api ] );
862
863     $t->get_ok( "//$userid:$password@/api/v1/holds/"
864           . $hold_2->id
865           . "/pickup_locations" )
866       ->json_is( [ $library_1_api, $library_2_api ] );
867
868     # filtering works!
869     $t->get_ok( "//$userid:$password@/api/v1/holds/"
870           . $hold_2->id
871           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
872       ->json_is( [ $library_1_api ] );
873
874     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
875
876     my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } });
877     my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } });
878
879     my $library_5_api = $library_5->to_api();
880     $library_5_api->{needs_override} = Mojo::JSON->true;
881
882     # bibli-level mock doesn't include library_1 as valid pickup location
883     $library_1_api->{needs_override} = Mojo::JSON->true;
884
885     $t->get_ok( "//$userid:$password@/api/v1/holds/"
886           . $hold_1->id
887           . "/pickup_locations?_order_by=marc_org_code" )
888       ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] );
889
890     $schema->storage->txn_rollback;
891 };
892
893 subtest 'edit() tests' => sub {
894
895     plan tests => 20;
896
897     $schema->storage->txn_begin;
898
899     my $password = 'AbcdEFG123';
900
901     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
902     my $patron = $builder->build_object(
903         { class => 'Koha::Patrons', value => { flags => 1 } } );
904     $patron->set_password( { password => $password, skip_validation => 1 } );
905     my $userid = $patron->userid;
906     $builder->build(
907         {
908             source => 'UserPermission',
909             value  => {
910                 borrowernumber => $patron->borrowernumber,
911                 module_bit     => 6,
912                 code           => 'modify_holds_priority',
913             },
914         }
915     );
916
917     # Disable logging
918     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
919     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
920     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
921
922     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
923     my $mock_item   = Test::MockModule->new('Koha::Item');
924
925     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
926     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
927     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
928
929     # let's control what Koha::Biblio->pickup_locations returns, for testing
930     $mock_biblio->mock( 'pickup_locations', sub {
931         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
932     });
933     # let's mock what Koha::Item->pickup_locations returns, for testing
934     $mock_item->mock( 'pickup_locations', sub {
935         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
936     });
937
938     my $biblio = $builder->build_sample_biblio;
939     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
940
941     # Test biblio-level holds
942     my $biblio_hold = $builder->build_object(
943         {
944             class => "Koha::Holds",
945             value => {
946                 biblionumber => $biblio->biblionumber,
947                 branchcode   => $library_3->branchcode,
948                 itemnumber   => undef,
949                 priority     => 1,
950             }
951         }
952     );
953
954     my $biblio_hold_api_data = $biblio_hold->to_api;
955     my $biblio_hold_data = {
956         pickup_library_id => $library_1->branchcode,
957         priority          => $biblio_hold_api_data->{priority}
958     };
959
960     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
961           . $biblio_hold->id
962           => json => $biblio_hold_data )
963       ->status_is(400)
964       ->json_is({ error => 'The supplied pickup location is not valid' });
965
966     $biblio_hold->discard_changes;
967     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
968
969     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
970           => { 'x-koha-override' => 'any' }
971           => json => $biblio_hold_data )
972       ->status_is(200)
973       ->json_has( '/pickup_library_id' => $library_1->id );
974
975     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
976     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
977           . $biblio_hold->id
978           => json => $biblio_hold_data )
979       ->status_is(200);
980
981     $biblio_hold->discard_changes;
982     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
983
984     # Test item-level holds
985     my $item_hold = $builder->build_object(
986         {
987             class => "Koha::Holds",
988             value => {
989                 biblionumber => $biblio->biblionumber,
990                 branchcode   => $library_3->branchcode,
991                 itemnumber   => $item->itemnumber,
992                 priority     => 1,
993             }
994         }
995     );
996
997     my $item_hold_api_data = $item_hold->to_api;
998     my $item_hold_data = {
999         pickup_library_id => $library_1->branchcode,
1000         priority          => $item_hold_api_data->{priority}
1001     };
1002
1003     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1004           . $item_hold->id
1005           => json => $item_hold_data )
1006       ->status_is(400)
1007       ->json_is({ error => 'The supplied pickup location is not valid' });
1008
1009     $item_hold->discard_changes;
1010     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
1011
1012     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1013           => { 'x-koha-override' => 'any' }
1014           => json => $item_hold_data )
1015       ->status_is(200)
1016       ->json_has( '/pickup_library_id' => $library_1->id );
1017
1018     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1019     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1020           . $item_hold->id
1021           => json => $item_hold_data )
1022       ->status_is(200);
1023
1024     $item_hold->discard_changes;
1025     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1026
1027     $schema->storage->txn_rollback;
1028 };
1029
1030 subtest 'add() tests' => sub {
1031
1032     plan tests => 10;
1033
1034     $schema->storage->txn_begin;
1035
1036     my $password = 'AbcdEFG123';
1037
1038     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
1039     my $patron = $builder->build_object(
1040         { class => 'Koha::Patrons', value => { flags => 1 } } );
1041     $patron->set_password( { password => $password, skip_validation => 1 } );
1042     my $userid = $patron->userid;
1043     $builder->build(
1044         {
1045             source => 'UserPermission',
1046             value  => {
1047                 borrowernumber => $patron->borrowernumber,
1048                 module_bit     => 6,
1049                 code           => 'modify_holds_priority',
1050             },
1051         }
1052     );
1053
1054     # Disable logging
1055     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1056     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1057
1058     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
1059     my $mock_item   = Test::MockModule->new('Koha::Item');
1060
1061     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1062     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1063     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1064
1065     # let's control what Koha::Biblio->pickup_locations returns, for testing
1066     $mock_biblio->mock( 'pickup_locations', sub {
1067         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1068     });
1069     # let's mock what Koha::Item->pickup_locations returns, for testing
1070     $mock_item->mock( 'pickup_locations', sub {
1071         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1072     });
1073
1074     my $can_be_reserved = 'OK';
1075     my $mock_reserves = Test::MockModule->new('C4::Reserves');
1076     $mock_reserves->mock( 'CanItemBeReserved', sub
1077         {
1078             return { status => $can_be_reserved }
1079         }
1080
1081     );
1082     $mock_reserves->mock( 'CanBookBeReserved', sub
1083         {
1084             return { status => $can_be_reserved }
1085         }
1086
1087     );
1088
1089     my $biblio = $builder->build_sample_biblio;
1090     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1091
1092     # Test biblio-level holds
1093     my $biblio_hold = $builder->build_object(
1094         {
1095             class => "Koha::Holds",
1096             value => {
1097                 biblionumber => $biblio->biblionumber,
1098                 branchcode   => $library_3->branchcode,
1099                 itemnumber   => undef,
1100                 priority     => 1,
1101             }
1102         }
1103     );
1104
1105     my $biblio_hold_api_data = $biblio_hold->to_api;
1106     $biblio_hold->delete;
1107     my $biblio_hold_data = {
1108         biblio_id         => $biblio_hold_api_data->{biblio_id},
1109         patron_id         => $biblio_hold_api_data->{patron_id},
1110         pickup_library_id => $library_1->branchcode,
1111     };
1112
1113     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1114       ->status_is(400)
1115       ->json_is({ error => 'The supplied pickup location is not valid' });
1116
1117     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1118     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1119       ->status_is(201);
1120
1121     # Test item-level holds
1122     my $item_hold = $builder->build_object(
1123         {
1124             class => "Koha::Holds",
1125             value => {
1126                 biblionumber => $biblio->biblionumber,
1127                 branchcode   => $library_3->branchcode,
1128                 itemnumber   => $item->itemnumber,
1129                 priority     => 1,
1130             }
1131         }
1132     );
1133
1134     my $item_hold_api_data = $item_hold->to_api;
1135     $item_hold->delete;
1136     my $item_hold_data = {
1137         biblio_id         => $item_hold_api_data->{biblio_id},
1138         item_id           => $item_hold_api_data->{item_id},
1139         patron_id         => $item_hold_api_data->{patron_id},
1140         pickup_library_id => $library_1->branchcode,
1141     };
1142
1143     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1144       ->status_is(400)
1145       ->json_is({ error => 'The supplied pickup location is not valid' });
1146
1147     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1148     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1149       ->status_is(201);
1150
1151     $schema->storage->txn_rollback;
1152 };
1153
1154 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1155
1156     plan tests => 28;
1157
1158     $schema->storage->txn_begin;
1159
1160     my $password = 'AbcdEFG123';
1161
1162     # library_1 and library_2 are available pickup locations, not library_3
1163     my $library_1 = $builder->build_object(
1164         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1165     my $library_2 = $builder->build_object(
1166         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1167     my $library_3 = $builder->build_object(
1168         { class => 'Koha::Libraries', value => { pickup_location => 0 } } );
1169
1170     my $patron = $builder->build_object(
1171         { class => 'Koha::Patrons', value => { flags => 0 } } );
1172     $patron->set_password( { password => $password, skip_validation => 1 } );
1173     my $userid = $patron->userid;
1174     $builder->build(
1175         {
1176             source => 'UserPermission',
1177             value  => {
1178                 borrowernumber => $patron->borrowernumber,
1179                 module_bit     => 6,
1180                 code           => 'place_holds',
1181             },
1182         }
1183     );
1184
1185     # Disable logging
1186     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1187     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1188
1189     my $biblio = $builder->build_sample_biblio;
1190     my $item   = $builder->build_sample_item(
1191         {
1192             biblionumber => $biblio->biblionumber,
1193             library      => $library_1->branchcode
1194         }
1195     );
1196
1197     # biblio-level hold
1198     my $hold = Koha::Holds->find(
1199         AddReserve(
1200             {
1201                 branchcode     => $library_1->branchcode,
1202                 borrowernumber => $patron->borrowernumber,
1203                 biblionumber   => $biblio->biblionumber,
1204                 priority       => 1,
1205                 itemnumber     => undef,
1206             }
1207         )
1208     );
1209
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(200)
1214       ->json_is({ pickup_library_id => $library_2->branchcode });
1215
1216     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1217
1218     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1219           . $hold->id
1220           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1221       ->status_is(400)
1222       ->json_is({ error => '[The supplied pickup location is not valid]' });
1223
1224     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1225
1226     # item-level hold
1227     $hold = Koha::Holds->find(
1228         AddReserve(
1229             {
1230                 branchcode     => $library_1->branchcode,
1231                 borrowernumber => $patron->borrowernumber,
1232                 biblionumber   => $biblio->biblionumber,
1233                 priority       => 1,
1234                 itemnumber     => $item->itemnumber,
1235             }
1236         )
1237     );
1238
1239     # Attempt to use an invalid pickup locations ends in 400
1240     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1241           . $hold->id
1242           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1243       ->status_is(400)
1244       ->json_is({ error => '[The supplied pickup location is not valid]' });
1245
1246     is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' );
1247
1248     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
1249
1250     # Attempt to use an invalid pickup locations with override succeeds
1251     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1252           . $hold->id
1253           . "/pickup_location"
1254           => { 'x-koha-override' => 'any' }
1255           => json => { pickup_library_id => $library_2->branchcode } )
1256       ->status_is(200)
1257       ->json_is({ pickup_library_id => $library_2->branchcode });
1258
1259     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library changed' );
1260
1261     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
1262
1263     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1264           . $hold->id
1265           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1266       ->status_is(200)
1267       ->json_is({ pickup_library_id => $library_2->branchcode });
1268
1269     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1270
1271     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1272           . $hold->id
1273           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1274       ->status_is(400)
1275       ->json_is({ error => '[The supplied pickup location is not valid]' });
1276
1277     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used' );
1278
1279     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1280           . $hold->id
1281           . "/pickup_location"
1282           => { 'x-koha-override' => 'any' }
1283           => json => { pickup_library_id => $library_3->branchcode } )
1284       ->status_is(400)
1285       ->json_is({ error => '[The supplied pickup location is not valid]' });
1286
1287     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used, even if x-koha-override is passed' );
1288
1289     $schema->storage->txn_rollback;
1290 };