Bug 30780: 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 => 14;
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 qw( AddReserve CanItemBeReserved CanBookBeReserved );
32 use C4::Items;
33
34 use Koha::Database;
35 use Koha::DateUtils qw( dt_from_string output_pref );
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 => 'iso', 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 => 'iso', 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 => 'iso', dateonly => 1 } ),
333         hold_date         => output_pref( { dt => $future_hold_date, dateformat => 'iso', 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 => 'iso', 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 => 0 } } );
465     $builder->build(
466         {
467             source => 'UserPermission',
468             value  => {
469                 borrowernumber => $patron->borrowernumber,
470                 module_bit     => 6,
471                 code           => 'place_holds',
472             },
473         }
474     );
475
476     $patron->set_password({ password => $password, skip_validation => 1 });
477     my $userid = $patron->userid;
478
479     # Disable logging
480     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
481     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
482
483     my $hold = $builder->build_object(
484         {   class => 'Koha::Holds',
485             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
486         }
487     );
488
489     ok( !$hold->is_suspended, 'Hold is not suspended' );
490     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
491         ->status_is( 201, 'Hold suspension created' );
492
493     $hold->discard_changes;    # refresh object
494
495     ok( $hold->is_suspended, 'Hold is suspended' );
496     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
497
498     my $end_date = output_pref({
499       dt         => dt_from_string( undef ),
500       dateformat => 'iso',
501       dateonly   => 1
502     });
503
504     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
505
506     $hold->discard_changes;    # refresh object
507
508     ok( $hold->is_suspended, 'Hold is suspended' );
509     $t->json_is(
510       '/end_date',
511       output_pref({
512         dt         => dt_from_string( $hold->suspend_until ),
513         dateformat => 'iso',
514         dateonly   => 1
515       }),
516       'Hold suspension has correct end date'
517     );
518
519     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
520       ->status_is(204, 'SWAGGER3.2.4')
521       ->content_is('', 'SWAGGER3.3.4');
522
523     # Pass a an expiration date for the suspension
524     my $date = dt_from_string()->add( days => 5 );
525     $t->post_ok(
526               "//$userid:$password@/api/v1/holds/"
527             . $hold->id
528             . "/suspension" => json => {
529             end_date =>
530                 output_pref( { dt => $date, dateformat => 'iso', dateonly => 1 } )
531             }
532     )->status_is( 201, 'Hold suspension created' )
533         ->json_is( '/end_date',
534         output_pref( { dt => $date, dateformat => 'iso', dateonly => 1 } ) )
535         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
536
537     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
538       ->status_is(204, 'SWAGGER3.2.4')
539       ->content_is('', 'SWAGGER3.3.4');
540
541     $hold->set_waiting->discard_changes;
542
543     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
544       ->status_is( 400, 'Cannot suspend waiting hold' )
545       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
546
547     $hold->set_transfer->discard_changes;
548
549     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
550       ->status_is( 400, 'Cannot suspend hold on transfer' )
551       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
552
553     $schema->storage->txn_rollback;
554 };
555
556 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
557
558     plan tests => 14;
559
560     $schema->storage->txn_begin;
561
562     my $password = 'AbcdEFG123';
563
564     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
565     my $patron_np = $builder->build_object(
566         { class => 'Koha::Patrons', value => { flags => 0 } } );
567     $patron_np->set_password( { password => $password, skip_validation => 1 } );
568     my $userid_np = $patron_np->userid;
569
570     my $patron = $builder->build_object(
571         { class => 'Koha::Patrons', value => { flags => 0 } } );
572     $patron->set_password( { password => $password, skip_validation => 1 } );
573     my $userid = $patron->userid;
574     $builder->build(
575         {
576             source => 'UserPermission',
577             value  => {
578                 borrowernumber => $patron->borrowernumber,
579                 module_bit     => 6,
580                 code           => 'modify_holds_priority',
581             },
582         }
583     );
584
585     # Disable logging
586     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
587     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
588
589     my $biblio   = $builder->build_sample_biblio;
590     my $patron_1 = $builder->build_object(
591         {
592             class => 'Koha::Patrons',
593             value => { branchcode => $library->branchcode }
594         }
595     );
596     my $patron_2 = $builder->build_object(
597         {
598             class => 'Koha::Patrons',
599             value => { branchcode => $library->branchcode }
600         }
601     );
602     my $patron_3 = $builder->build_object(
603         {
604             class => 'Koha::Patrons',
605             value => { branchcode => $library->branchcode }
606         }
607     );
608
609     my $hold_1 = Koha::Holds->find(
610         AddReserve(
611             {
612                 branchcode     => $library->branchcode,
613                 borrowernumber => $patron_1->borrowernumber,
614                 biblionumber   => $biblio->biblionumber,
615                 priority       => 1,
616             }
617         )
618     );
619     my $hold_2 = Koha::Holds->find(
620         AddReserve(
621             {
622                 branchcode     => $library->branchcode,
623                 borrowernumber => $patron_2->borrowernumber,
624                 biblionumber   => $biblio->biblionumber,
625                 priority       => 2,
626             }
627         )
628     );
629     my $hold_3 = Koha::Holds->find(
630         AddReserve(
631             {
632                 branchcode     => $library->branchcode,
633                 borrowernumber => $patron_3->borrowernumber,
634                 biblionumber   => $biblio->biblionumber,
635                 priority       => 3,
636             }
637         )
638     );
639
640     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
641           . $hold_3->id
642           . "/priority" => json => 1 )->status_is(403);
643
644     $t->put_ok( "//$userid:$password@/api/v1/holds/"
645           . $hold_3->id
646           . "/priority" => json => 1 )->status_is(200)->json_is(1);
647
648     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
649     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
650     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
651
652     $t->put_ok( "//$userid:$password@/api/v1/holds/"
653           . $hold_3->id
654           . "/priority" => json => 3 )->status_is(200)->json_is(3);
655
656     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
657     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
658     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
659
660     $schema->storage->txn_rollback;
661 };
662
663 subtest 'add() tests (maxreserves behaviour)' => sub {
664
665     plan tests => 11;
666
667     $schema->storage->txn_begin;
668
669     $dbh->do('DELETE FROM reserves');
670
671     Koha::CirculationRules->new->delete;
672
673     my $password = 'AbcdEFG123';
674
675     my $patron = $builder->build_object(
676         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
677     $patron->set_password({ password => $password, skip_validation => 1 });
678     my $userid = $patron->userid;
679
680     Koha::CirculationRules->set_rules(
681         {
682             itemtype     => undef,
683             branchcode   => undef,
684             categorycode => undef,
685             rules        => {
686                 reservesallowed => 3
687             }
688         }
689     );
690
691     Koha::CirculationRules->set_rules(
692         {
693             branchcode   => undef,
694             categorycode => $patron->categorycode,
695             rules        => {
696                 max_holds   => 4,
697             }
698         }
699     );
700
701     my $biblio_1 = $builder->build_sample_biblio;
702     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
703     my $biblio_2 = $builder->build_sample_biblio;
704     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
705     my $biblio_3 = $builder->build_sample_biblio;
706     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
707
708     # Make sure pickup location checks doesn't get in the middle
709     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
710     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
711     my $mock_item   = Test::MockModule->new('Koha::Item');
712     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
713
714     # Disable logging
715     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
716     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
717     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
718     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
719
720     my $post_data = {
721         patron_id => $patron->borrowernumber,
722         biblio_id => $biblio_1->biblionumber,
723         pickup_library_id => $item_1->home_branch->branchcode,
724         item_type => $item_1->itype,
725     };
726
727     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
728       ->status_is(201);
729
730     $post_data = {
731         patron_id => $patron->borrowernumber,
732         biblio_id => $biblio_2->biblionumber,
733         pickup_library_id => $item_2->home_branch->branchcode,
734         item_id   => $item_2->itemnumber
735     };
736
737     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
738       ->status_is(201);
739
740     $post_data = {
741         patron_id => $patron->borrowernumber,
742         biblio_id => $biblio_3->biblionumber,
743         pickup_library_id => $item_1->home_branch->branchcode,
744         item_id   => $item_3->itemnumber
745     };
746
747     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
748       ->status_is(403)
749       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
750
751     t::lib::Mocks::mock_preference( 'maxreserves', 0 );
752
753     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
754       ->status_is(201, 'maxreserves == 0 => no limit');
755
756     # cleanup for the next tests
757     my $hold_id = $t->tx->res->json->{hold_id};
758     Koha::Holds->find( $hold_id )->delete;
759
760     t::lib::Mocks::mock_preference( 'maxreserves', undef );
761
762     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
763       ->status_is(201, 'maxreserves == undef => no limit');
764
765     $schema->storage->txn_rollback;
766 };
767
768 subtest 'pickup_locations() tests' => sub {
769
770     plan tests => 12;
771
772     $schema->storage->txn_begin;
773
774     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
775
776     # Small trick to ease testing
777     Koha::Libraries->search->update({ pickup_location => 0 });
778
779     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } });
780     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } });
781     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } });
782
783     my $library_1_api = $library_1->to_api();
784     my $library_2_api = $library_2->to_api();
785     my $library_3_api = $library_3->to_api();
786
787     $library_1_api->{needs_override} = Mojo::JSON->false;
788     $library_2_api->{needs_override} = Mojo::JSON->false;
789     $library_3_api->{needs_override} = Mojo::JSON->false;
790
791     my $patron = $builder->build_object(
792         {
793             class => 'Koha::Patrons',
794             value => { userid => 'tomasito', flags => 0 }
795         }
796     );
797     $patron->set_password( { password => $password, skip_validation => 1 } );
798     my $userid = $patron->userid;
799     $builder->build(
800         {
801             source => 'UserPermission',
802             value  => {
803                 borrowernumber => $patron->borrowernumber,
804                 module_bit     => 6,
805                 code           => 'place_holds',
806             },
807         }
808     );
809
810     my $item_class = Test::MockModule->new('Koha::Item');
811     $item_class->mock(
812         'pickup_locations',
813         sub {
814             my ( $self, $params ) = @_;
815             my $mock_patron = $params->{patron};
816             is( $mock_patron->borrowernumber,
817                 $patron->borrowernumber, 'Patron passed correctly' );
818             return Koha::Libraries->search(
819                 {
820                     branchcode => {
821                         '-in' => [
822                             $library_1->branchcode,
823                             $library_2->branchcode
824                         ]
825                     }
826                 },
827                 {   # we make sure no surprises in the order of the result
828                     order_by => { '-asc' => 'marcorgcode' }
829                 }
830             );
831         }
832     );
833
834     my $biblio_class = Test::MockModule->new('Koha::Biblio');
835     $biblio_class->mock(
836         'pickup_locations',
837         sub {
838             my ( $self, $params ) = @_;
839             my $mock_patron = $params->{patron};
840             is( $mock_patron->borrowernumber,
841                 $patron->borrowernumber, 'Patron passed correctly' );
842             return Koha::Libraries->search(
843                 {
844                     branchcode => {
845                         '-in' => [
846                             $library_2->branchcode,
847                             $library_3->branchcode
848                         ]
849                     }
850                 },
851                 {   # we make sure no surprises in the order of the result
852                     order_by => { '-asc' => 'marcorgcode' }
853                 }
854             );
855         }
856     );
857
858     my $item = $builder->build_sample_item;
859
860     # biblio-level hold
861     my $hold_1 = $builder->build_object(
862         {
863             class => 'Koha::Holds',
864             value => {
865                 itemnumber     => undef,
866                 biblionumber   => $item->biblionumber,
867                 borrowernumber => $patron->borrowernumber
868             }
869         }
870     );
871     # item-level hold
872     my $hold_2 = $builder->build_object(
873         {
874             class => 'Koha::Holds',
875             value => {
876                 itemnumber     => $item->itemnumber,
877                 biblionumber   => $item->biblionumber,
878                 borrowernumber => $patron->borrowernumber
879             }
880         }
881     );
882
883     $t->get_ok( "//$userid:$password@/api/v1/holds/"
884           . $hold_1->id
885           . "/pickup_locations" )
886       ->json_is( [ $library_2_api, $library_3_api ] );
887
888     $t->get_ok( "//$userid:$password@/api/v1/holds/"
889           . $hold_2->id
890           . "/pickup_locations" )
891       ->json_is( [ $library_1_api, $library_2_api ] );
892
893     # filtering works!
894     $t->get_ok( "//$userid:$password@/api/v1/holds/"
895           . $hold_2->id
896           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
897       ->json_is( [ $library_1_api ] );
898
899     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
900
901     my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } });
902     my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } });
903
904     my $library_5_api = $library_5->to_api();
905     $library_5_api->{needs_override} = Mojo::JSON->true;
906
907     # bibli-level mock doesn't include library_1 as valid pickup location
908     $library_1_api->{needs_override} = Mojo::JSON->true;
909
910     $t->get_ok( "//$userid:$password@/api/v1/holds/"
911           . $hold_1->id
912           . "/pickup_locations?_order_by=marc_org_code" )
913       ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] );
914
915     $schema->storage->txn_rollback;
916 };
917
918 subtest 'edit() tests' => sub {
919
920     plan tests => 39;
921
922     $schema->storage->txn_begin;
923
924     my $password = 'AbcdEFG123';
925
926     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
927     my $patron = $builder->build_object(
928         { class => 'Koha::Patrons', value => { flags => 1 } } );
929     $patron->set_password( { password => $password, skip_validation => 1 } );
930     my $userid = $patron->userid;
931     $builder->build(
932         {
933             source => 'UserPermission',
934             value  => {
935                 borrowernumber => $patron->borrowernumber,
936                 module_bit     => 6,
937                 code           => 'modify_holds_priority',
938             },
939         }
940     );
941
942     # Disable logging
943     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
944     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
945     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
946
947     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
948     my $mock_item   = Test::MockModule->new('Koha::Item');
949
950     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
951     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
952     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
953
954     # let's control what Koha::Biblio->pickup_locations returns, for testing
955     $mock_biblio->mock( 'pickup_locations', sub {
956         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
957     });
958     # let's mock what Koha::Item->pickup_locations returns, for testing
959     $mock_item->mock( 'pickup_locations', sub {
960         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
961     });
962
963     my $biblio = $builder->build_sample_biblio;
964     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
965
966     # Test biblio-level holds
967     my $biblio_hold = $builder->build_object(
968         {
969             class => "Koha::Holds",
970             value => {
971                 biblionumber => $biblio->biblionumber,
972                 branchcode   => $library_3->branchcode,
973                 itemnumber   => undef,
974                 priority     => 1,
975             }
976         }
977     );
978
979     my $biblio_hold_api_data = $biblio_hold->to_api;
980     my $biblio_hold_data = {
981         pickup_library_id => $library_1->branchcode,
982         priority          => $biblio_hold_api_data->{priority}
983     };
984
985     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
986           . $biblio_hold->id
987           => json => $biblio_hold_data )
988       ->status_is(400)
989       ->json_is({ error => 'The supplied pickup location is not valid' });
990
991     $t->put_ok( "//$userid:$password@/api/v1/holds/"
992           . $biblio_hold->id
993           => json => $biblio_hold_data )
994       ->status_is(400)
995       ->json_is({ error => 'The supplied pickup location is not valid' });
996
997     $biblio_hold->discard_changes;
998     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
999
1000     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
1001           => { 'x-koha-override' => 'any' }
1002           => json => $biblio_hold_data )
1003       ->status_is(200)
1004       ->json_has( '/pickup_library_id' => $library_1->id );
1005
1006     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
1007           => { 'x-koha-override' => 'any' }
1008           => json => $biblio_hold_data )
1009       ->status_is(200)
1010       ->json_has( '/pickup_library_id' => $library_1->id );
1011
1012     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1013     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1014           . $biblio_hold->id
1015           => json => $biblio_hold_data )
1016       ->status_is(200);
1017
1018     $biblio_hold->discard_changes;
1019     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1020
1021     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1022           . $biblio_hold->id
1023           => json => $biblio_hold_data )
1024       ->status_is(200);
1025
1026     $biblio_hold->discard_changes;
1027     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1028
1029     # Test item-level holds
1030     my $item_hold = $builder->build_object(
1031         {
1032             class => "Koha::Holds",
1033             value => {
1034                 biblionumber => $biblio->biblionumber,
1035                 branchcode   => $library_3->branchcode,
1036                 itemnumber   => $item->itemnumber,
1037                 priority     => 1,
1038                 suspend       => 0,
1039                 suspend_until => undef,
1040             }
1041         }
1042     );
1043
1044     my $item_hold_api_data = $item_hold->to_api;
1045     my $item_hold_data = {
1046         pickup_library_id => $library_1->branchcode,
1047         priority          => $item_hold_api_data->{priority}
1048     };
1049
1050     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1051           . $item_hold->id
1052           => json => $item_hold_data )
1053       ->status_is(400)
1054       ->json_is({ error => 'The supplied pickup location is not valid' });
1055
1056     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1057           . $item_hold->id
1058           => json => $item_hold_data )
1059       ->status_is(400)
1060       ->json_is({ error => 'The supplied pickup location is not valid' });
1061
1062     $item_hold->discard_changes;
1063     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
1064
1065     $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1066           => { 'x-koha-override' => 'any' }
1067           => json => $item_hold_data )
1068       ->status_is(200)
1069       ->json_has( '/pickup_library_id' => $library_1->id );
1070
1071     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1072           => { 'x-koha-override' => 'any' }
1073           => json => $item_hold_data )
1074       ->status_is(200)
1075       ->json_has( '/pickup_library_id' => $library_1->id );
1076
1077     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1078     $t->patch_ok( "//$userid:$password@/api/v1/holds/"
1079           . $item_hold->id
1080           => json => $item_hold_data )
1081       ->status_is(200);
1082
1083     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1084           . $item_hold->id
1085           => json => $item_hold_data )
1086       ->status_is(200);
1087
1088     $item_hold->discard_changes;
1089     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1090
1091     is( $item_hold->suspend, 0, 'Location change should not activate suspended status' );
1092     is( $item_hold->suspend_until, undef, 'Location change should keep suspended_until be undef' );
1093
1094     $schema->storage->txn_rollback;
1095
1096 };
1097
1098 subtest 'add() tests' => sub {
1099
1100     plan tests => 16;
1101
1102     $schema->storage->txn_begin;
1103
1104     my $password = 'AbcdEFG123';
1105
1106     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
1107     my $patron = $builder->build_object(
1108         { class => 'Koha::Patrons', value => { flags => 1 } } );
1109     $patron->set_password( { password => $password, skip_validation => 1 } );
1110     my $userid = $patron->userid;
1111     $builder->build(
1112         {
1113             source => 'UserPermission',
1114             value  => {
1115                 borrowernumber => $patron->borrowernumber,
1116                 module_bit     => 6,
1117                 code           => 'modify_holds_priority',
1118             },
1119         }
1120     );
1121
1122     # Disable logging
1123     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1124     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1125
1126     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
1127     my $mock_item   = Test::MockModule->new('Koha::Item');
1128
1129     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1130     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1131     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1132
1133     # let's control what Koha::Biblio->pickup_locations returns, for testing
1134     $mock_biblio->mock( 'pickup_locations', sub {
1135         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1136     });
1137     # let's mock what Koha::Item->pickup_locations returns, for testing
1138     $mock_item->mock( 'pickup_locations', sub {
1139         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1140     });
1141
1142     my $can_be_reserved = 'OK';
1143     my $mock_reserves = Test::MockModule->new('C4::Reserves');
1144     $mock_reserves->mock( 'CanItemBeReserved', sub
1145         {
1146             return { status => $can_be_reserved }
1147         }
1148
1149     );
1150     $mock_reserves->mock( 'CanBookBeReserved', sub
1151         {
1152             return { status => $can_be_reserved }
1153         }
1154
1155     );
1156
1157     my $biblio = $builder->build_sample_biblio;
1158     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1159
1160     # Test biblio-level holds
1161     my $biblio_hold = $builder->build_object(
1162         {
1163             class => "Koha::Holds",
1164             value => {
1165                 biblionumber => $biblio->biblionumber,
1166                 branchcode   => $library_3->branchcode,
1167                 itemnumber   => undef,
1168                 priority     => 1,
1169             }
1170         }
1171     );
1172
1173     my $biblio_hold_api_data = $biblio_hold->to_api;
1174     $biblio_hold->delete;
1175     my $biblio_hold_data = {
1176         biblio_id         => $biblio_hold_api_data->{biblio_id},
1177         patron_id         => $biblio_hold_api_data->{patron_id},
1178         pickup_library_id => $library_1->branchcode,
1179     };
1180
1181     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1182       ->status_is(400)
1183       ->json_is({ error => 'The supplied pickup location is not valid' });
1184
1185     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1186     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1187       ->status_is(201);
1188
1189     # Test item-level holds
1190     my $item_hold = $builder->build_object(
1191         {
1192             class => "Koha::Holds",
1193             value => {
1194                 biblionumber => $biblio->biblionumber,
1195                 branchcode   => $library_3->branchcode,
1196                 itemnumber   => $item->itemnumber,
1197                 priority     => 1,
1198             }
1199         }
1200     );
1201
1202     my $item_hold_api_data = $item_hold->to_api;
1203     $item_hold->delete;
1204     my $item_hold_data = {
1205         biblio_id         => $item_hold_api_data->{biblio_id},
1206         item_id           => $item_hold_api_data->{item_id},
1207         patron_id         => $item_hold_api_data->{patron_id},
1208         pickup_library_id => $library_1->branchcode,
1209     };
1210
1211     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1212       ->status_is(400)
1213       ->json_is({ error => 'The supplied pickup location is not valid' });
1214
1215     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1216     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1217       ->status_is(201);
1218
1219     # empty cases
1220     $mock_biblio->mock( 'pickup_locations', sub {
1221         return Koha::Libraries->new->empty;
1222     });
1223
1224     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1225       ->status_is(400)
1226       ->json_is({ error => 'The supplied pickup location is not valid' });
1227
1228     # empty cases
1229     $mock_item->mock( 'pickup_locations', sub {
1230         return Koha::Libraries->new->empty;
1231     });
1232
1233     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1234       ->status_is(400)
1235       ->json_is({ error => 'The supplied pickup location is not valid' });
1236
1237     $schema->storage->txn_rollback;
1238 };
1239
1240 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1241
1242     plan tests => 28;
1243
1244     $schema->storage->txn_begin;
1245
1246     my $password = 'AbcdEFG123';
1247
1248     # library_1 and library_2 are available pickup locations, not library_3
1249     my $library_1 = $builder->build_object(
1250         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1251     my $library_2 = $builder->build_object(
1252         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1253     my $library_3 = $builder->build_object(
1254         { class => 'Koha::Libraries', value => { pickup_location => 0 } } );
1255
1256     my $patron = $builder->build_object(
1257         { class => 'Koha::Patrons', value => { flags => 0 } } );
1258     $patron->set_password( { password => $password, skip_validation => 1 } );
1259     my $userid = $patron->userid;
1260     $builder->build(
1261         {
1262             source => 'UserPermission',
1263             value  => {
1264                 borrowernumber => $patron->borrowernumber,
1265                 module_bit     => 6,
1266                 code           => 'place_holds',
1267             },
1268         }
1269     );
1270
1271     # Disable logging
1272     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1273     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1274
1275     my $biblio = $builder->build_sample_biblio;
1276     my $item   = $builder->build_sample_item(
1277         {
1278             biblionumber => $biblio->biblionumber,
1279             library      => $library_1->branchcode
1280         }
1281     );
1282
1283     # biblio-level hold
1284     my $hold = Koha::Holds->find(
1285         AddReserve(
1286             {
1287                 branchcode     => $library_1->branchcode,
1288                 borrowernumber => $patron->borrowernumber,
1289                 biblionumber   => $biblio->biblionumber,
1290                 priority       => 1,
1291                 itemnumber     => undef,
1292             }
1293         )
1294     );
1295
1296     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1297           . $hold->id
1298           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1299       ->status_is(200)
1300       ->json_is({ pickup_library_id => $library_2->branchcode });
1301
1302     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1303
1304     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1305           . $hold->id
1306           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1307       ->status_is(400)
1308       ->json_is({ error => '[The supplied pickup location is not valid]' });
1309
1310     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1311
1312     # item-level hold
1313     $hold = Koha::Holds->find(
1314         AddReserve(
1315             {
1316                 branchcode     => $library_1->branchcode,
1317                 borrowernumber => $patron->borrowernumber,
1318                 biblionumber   => $biblio->biblionumber,
1319                 priority       => 1,
1320                 itemnumber     => $item->itemnumber,
1321             }
1322         )
1323     );
1324
1325     # Attempt to use an invalid pickup locations ends in 400
1326     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1327           . $hold->id
1328           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1329       ->status_is(400)
1330       ->json_is({ error => '[The supplied pickup location is not valid]' });
1331
1332     is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' );
1333
1334     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
1335
1336     # Attempt to use an invalid pickup locations with override succeeds
1337     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1338           . $hold->id
1339           . "/pickup_location"
1340           => { 'x-koha-override' => 'any' }
1341           => json => { pickup_library_id => $library_2->branchcode } )
1342       ->status_is(200)
1343       ->json_is({ pickup_library_id => $library_2->branchcode });
1344
1345     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library changed' );
1346
1347     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
1348
1349     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1350           . $hold->id
1351           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1352       ->status_is(200)
1353       ->json_is({ pickup_library_id => $library_2->branchcode });
1354
1355     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1356
1357     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1358           . $hold->id
1359           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1360       ->status_is(400)
1361       ->json_is({ error => '[The supplied pickup location is not valid]' });
1362
1363     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used' );
1364
1365     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1366           . $hold->id
1367           . "/pickup_location"
1368           => { 'x-koha-override' => 'any' }
1369           => json => { pickup_library_id => $library_3->branchcode } )
1370       ->status_is(400)
1371       ->json_is({ error => '[The supplied pickup location is not valid]' });
1372
1373     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used, even if x-koha-override is passed' );
1374
1375     $schema->storage->txn_rollback;
1376 };
1377
1378 subtest 'delete() tests' => sub {
1379
1380     plan tests => 3;
1381
1382     $schema->storage->txn_begin;
1383
1384     my $password = 'AbcdEFG123';
1385     my $patron   = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 0 } });
1386     $patron->set_password({ password => $password, skip_validation => 1 });
1387     my $userid = $patron->userid;
1388
1389     # Only have 'place_holds' subpermission
1390     $builder->build(
1391         {
1392             source => 'UserPermission',
1393             value  => {
1394                 borrowernumber => $patron->borrowernumber,
1395                 module_bit     => 6,
1396                 code           => 'place_holds',
1397             },
1398         }
1399     );
1400
1401     # Disable logging
1402     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1403     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1404
1405     my $biblio = $builder->build_sample_biblio;
1406     my $item   = $builder->build_sample_item(
1407         {
1408             biblionumber => $biblio->biblionumber,
1409             library      => $patron->branchcode
1410         }
1411     );
1412
1413     # Add a hold
1414     my $hold = Koha::Holds->find(
1415         AddReserve(
1416             {
1417                 branchcode     => $patron->branchcode,
1418                 borrowernumber => $patron->borrowernumber,
1419                 biblionumber   => $biblio->biblionumber,
1420                 priority       => 1,
1421                 itemnumber     => undef,
1422             }
1423         )
1424     );
1425
1426     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id )
1427       ->status_is(204, 'SWAGGER3.2.4')
1428       ->content_is('', 'SWAGGER3.3.4');
1429
1430     $schema->storage->txn_rollback;
1431 };