e5a84bae3c5fbf1a50d99ddd9ddbf440c4f8f368
[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         => $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 => 37;
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     $t->put_ok( "//$userid:$password@/api/v1/holds/"
967           . $biblio_hold->id
968           => json => $biblio_hold_data )
969       ->status_is(400)
970       ->json_is({ error => 'The supplied pickup location is not valid' });
971
972     $biblio_hold->discard_changes;
973     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
974
975     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
976           => { 'x-koha-override' => 'any' }
977           => json => $biblio_hold_data )
978       ->status_is(200)
979       ->json_has( '/pickup_library_id' => $library_1->id );
980
981     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
982           => { 'x-koha-override' => 'any' }
983           => json => $biblio_hold_data )
984       ->status_is(200)
985       ->json_has( '/pickup_library_id' => $library_1->id );
986
987     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
988     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
989           . $biblio_hold->id
990           => json => $biblio_hold_data )
991       ->status_is(200);
992
993     $biblio_hold->discard_changes;
994     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
995
996     $t->put_ok( "//$userid:$password@/api/v1/holds/"
997           . $biblio_hold->id
998           => json => $biblio_hold_data )
999       ->status_is(200);
1000
1001     $biblio_hold->discard_changes;
1002     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1003
1004     # Test item-level holds
1005     my $item_hold = $builder->build_object(
1006         {
1007             class => "Koha::Holds",
1008             value => {
1009                 biblionumber => $biblio->biblionumber,
1010                 branchcode   => $library_3->branchcode,
1011                 itemnumber   => $item->itemnumber,
1012                 priority     => 1,
1013             }
1014         }
1015     );
1016
1017     my $item_hold_api_data = $item_hold->to_api;
1018     my $item_hold_data = {
1019         pickup_library_id => $library_1->branchcode,
1020         priority          => $item_hold_api_data->{priority}
1021     };
1022
1023     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1024           . $item_hold->id
1025           => json => $item_hold_data )
1026       ->status_is(400)
1027       ->json_is({ error => 'The supplied pickup location is not valid' });
1028
1029     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1030           . $item_hold->id
1031           => json => $item_hold_data )
1032       ->status_is(400)
1033       ->json_is({ error => 'The supplied pickup location is not valid' });
1034
1035     $item_hold->discard_changes;
1036     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
1037
1038     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1039           => { 'x-koha-override' => 'any' }
1040           => json => $item_hold_data )
1041       ->status_is(200)
1042       ->json_has( '/pickup_library_id' => $library_1->id );
1043
1044     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1045           => { 'x-koha-override' => 'any' }
1046           => json => $item_hold_data )
1047       ->status_is(200)
1048       ->json_has( '/pickup_library_id' => $library_1->id );
1049
1050     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1051     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1052           . $item_hold->id
1053           => json => $item_hold_data )
1054       ->status_is(200);
1055
1056     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1057           . $item_hold->id
1058           => json => $item_hold_data )
1059       ->status_is(200);
1060
1061     $item_hold->discard_changes;
1062     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1063
1064     $schema->storage->txn_rollback;
1065 };
1066
1067 subtest 'add() tests' => sub {
1068
1069     plan tests => 10;
1070
1071     $schema->storage->txn_begin;
1072
1073     my $password = 'AbcdEFG123';
1074
1075     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
1076     my $patron = $builder->build_object(
1077         { class => 'Koha::Patrons', value => { flags => 1 } } );
1078     $patron->set_password( { password => $password, skip_validation => 1 } );
1079     my $userid = $patron->userid;
1080     $builder->build(
1081         {
1082             source => 'UserPermission',
1083             value  => {
1084                 borrowernumber => $patron->borrowernumber,
1085                 module_bit     => 6,
1086                 code           => 'modify_holds_priority',
1087             },
1088         }
1089     );
1090
1091     # Disable logging
1092     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1093     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1094
1095     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
1096     my $mock_item   = Test::MockModule->new('Koha::Item');
1097
1098     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1099     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1100     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1101
1102     # let's control what Koha::Biblio->pickup_locations returns, for testing
1103     $mock_biblio->mock( 'pickup_locations', sub {
1104         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1105     });
1106     # let's mock what Koha::Item->pickup_locations returns, for testing
1107     $mock_item->mock( 'pickup_locations', sub {
1108         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1109     });
1110
1111     my $can_be_reserved = 'OK';
1112     my $mock_reserves = Test::MockModule->new('C4::Reserves');
1113     $mock_reserves->mock( 'CanItemBeReserved', sub
1114         {
1115             return { status => $can_be_reserved }
1116         }
1117
1118     );
1119     $mock_reserves->mock( 'CanBookBeReserved', sub
1120         {
1121             return { status => $can_be_reserved }
1122         }
1123
1124     );
1125
1126     my $biblio = $builder->build_sample_biblio;
1127     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1128
1129     # Test biblio-level holds
1130     my $biblio_hold = $builder->build_object(
1131         {
1132             class => "Koha::Holds",
1133             value => {
1134                 biblionumber => $biblio->biblionumber,
1135                 branchcode   => $library_3->branchcode,
1136                 itemnumber   => undef,
1137                 priority     => 1,
1138             }
1139         }
1140     );
1141
1142     my $biblio_hold_api_data = $biblio_hold->to_api;
1143     $biblio_hold->delete;
1144     my $biblio_hold_data = {
1145         biblio_id         => $biblio_hold_api_data->{biblio_id},
1146         patron_id         => $biblio_hold_api_data->{patron_id},
1147         pickup_library_id => $library_1->branchcode,
1148     };
1149
1150     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1151       ->status_is(400)
1152       ->json_is({ error => 'The supplied pickup location is not valid' });
1153
1154     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1155     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1156       ->status_is(201);
1157
1158     # Test item-level holds
1159     my $item_hold = $builder->build_object(
1160         {
1161             class => "Koha::Holds",
1162             value => {
1163                 biblionumber => $biblio->biblionumber,
1164                 branchcode   => $library_3->branchcode,
1165                 itemnumber   => $item->itemnumber,
1166                 priority     => 1,
1167             }
1168         }
1169     );
1170
1171     my $item_hold_api_data = $item_hold->to_api;
1172     $item_hold->delete;
1173     my $item_hold_data = {
1174         biblio_id         => $item_hold_api_data->{biblio_id},
1175         item_id           => $item_hold_api_data->{item_id},
1176         patron_id         => $item_hold_api_data->{patron_id},
1177         pickup_library_id => $library_1->branchcode,
1178     };
1179
1180     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1181       ->status_is(400)
1182       ->json_is({ error => 'The supplied pickup location is not valid' });
1183
1184     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1185     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1186       ->status_is(201);
1187
1188     $schema->storage->txn_rollback;
1189 };
1190
1191 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1192
1193     plan tests => 28;
1194
1195     $schema->storage->txn_begin;
1196
1197     my $password = 'AbcdEFG123';
1198
1199     # library_1 and library_2 are available pickup locations, not library_3
1200     my $library_1 = $builder->build_object(
1201         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1202     my $library_2 = $builder->build_object(
1203         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1204     my $library_3 = $builder->build_object(
1205         { class => 'Koha::Libraries', value => { pickup_location => 0 } } );
1206
1207     my $patron = $builder->build_object(
1208         { class => 'Koha::Patrons', value => { flags => 0 } } );
1209     $patron->set_password( { password => $password, skip_validation => 1 } );
1210     my $userid = $patron->userid;
1211     $builder->build(
1212         {
1213             source => 'UserPermission',
1214             value  => {
1215                 borrowernumber => $patron->borrowernumber,
1216                 module_bit     => 6,
1217                 code           => 'place_holds',
1218             },
1219         }
1220     );
1221
1222     # Disable logging
1223     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1224     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1225
1226     my $biblio = $builder->build_sample_biblio;
1227     my $item   = $builder->build_sample_item(
1228         {
1229             biblionumber => $biblio->biblionumber,
1230             library      => $library_1->branchcode
1231         }
1232     );
1233
1234     # biblio-level hold
1235     my $hold = Koha::Holds->find(
1236         AddReserve(
1237             {
1238                 branchcode     => $library_1->branchcode,
1239                 borrowernumber => $patron->borrowernumber,
1240                 biblionumber   => $biblio->biblionumber,
1241                 priority       => 1,
1242                 itemnumber     => undef,
1243             }
1244         )
1245     );
1246
1247     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1248           . $hold->id
1249           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1250       ->status_is(200)
1251       ->json_is({ pickup_library_id => $library_2->branchcode });
1252
1253     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1254
1255     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1256           . $hold->id
1257           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1258       ->status_is(400)
1259       ->json_is({ error => '[The supplied pickup location is not valid]' });
1260
1261     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1262
1263     # item-level hold
1264     $hold = Koha::Holds->find(
1265         AddReserve(
1266             {
1267                 branchcode     => $library_1->branchcode,
1268                 borrowernumber => $patron->borrowernumber,
1269                 biblionumber   => $biblio->biblionumber,
1270                 priority       => 1,
1271                 itemnumber     => $item->itemnumber,
1272             }
1273         )
1274     );
1275
1276     # Attempt to use an invalid pickup locations ends in 400
1277     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1278           . $hold->id
1279           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1280       ->status_is(400)
1281       ->json_is({ error => '[The supplied pickup location is not valid]' });
1282
1283     is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' );
1284
1285     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
1286
1287     # Attempt to use an invalid pickup locations with override succeeds
1288     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1289           . $hold->id
1290           . "/pickup_location"
1291           => { 'x-koha-override' => 'any' }
1292           => json => { pickup_library_id => $library_2->branchcode } )
1293       ->status_is(200)
1294       ->json_is({ pickup_library_id => $library_2->branchcode });
1295
1296     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library changed' );
1297
1298     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
1299
1300     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1301           . $hold->id
1302           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1303       ->status_is(200)
1304       ->json_is({ pickup_library_id => $library_2->branchcode });
1305
1306     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1307
1308     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1309           . $hold->id
1310           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1311       ->status_is(400)
1312       ->json_is({ error => '[The supplied pickup location is not valid]' });
1313
1314     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used' );
1315
1316     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1317           . $hold->id
1318           . "/pickup_location"
1319           => { 'x-koha-override' => 'any' }
1320           => json => { pickup_library_id => $library_3->branchcode } )
1321       ->status_is(400)
1322       ->json_is({ error => '[The supplied pickup location is not valid]' });
1323
1324     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used, even if x-koha-override is passed' );
1325
1326     $schema->storage->txn_rollback;
1327 };