Bug 34365: Unit 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_2:$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_3:$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 => 21;
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 biblio-level holds
1190     my $item_group = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
1191     $biblio_hold = $builder->build_object(
1192         {
1193             class => "Koha::Holds",
1194             value => {
1195                 biblionumber  => $biblio->biblionumber,
1196                 branchcode    => $library_3->branchcode,
1197                 itemnumber    => undef,
1198                 priority      => 1,
1199                 item_group_id => $item_group->id,
1200             }
1201         }
1202     );
1203
1204     $biblio_hold_api_data = $biblio_hold->to_api;
1205     $biblio_hold->delete;
1206     $biblio_hold_data = {
1207         biblio_id         => $biblio_hold_api_data->{biblio_id},
1208         patron_id         => $biblio_hold_api_data->{patron_id},
1209         pickup_library_id => $library_1->branchcode,
1210     };
1211
1212     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1213       ->status_is(400)
1214       ->json_is({ error => 'The supplied pickup location is not valid' });
1215
1216     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1217     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1218       ->status_is(201);
1219
1220     # Test item-level holds
1221     my $item_hold = $builder->build_object(
1222         {
1223             class => "Koha::Holds",
1224             value => {
1225                 biblionumber => $biblio->biblionumber,
1226                 branchcode   => $library_3->branchcode,
1227                 itemnumber   => $item->itemnumber,
1228                 priority     => 1,
1229             }
1230         }
1231     );
1232
1233     my $item_hold_api_data = $item_hold->to_api;
1234     $item_hold->delete;
1235     my $item_hold_data = {
1236         biblio_id         => $item_hold_api_data->{biblio_id},
1237         item_id           => $item_hold_api_data->{item_id},
1238         patron_id         => $item_hold_api_data->{patron_id},
1239         pickup_library_id => $library_1->branchcode,
1240     };
1241
1242     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1243       ->status_is(400)
1244       ->json_is({ error => 'The supplied pickup location is not valid' });
1245
1246     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1247     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1248       ->status_is(201);
1249
1250     # empty cases
1251     $mock_biblio->mock( 'pickup_locations', sub {
1252         return Koha::Libraries->new->empty;
1253     });
1254
1255     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1256       ->status_is(400)
1257       ->json_is({ error => 'The supplied pickup location is not valid' });
1258
1259     # empty cases
1260     $mock_item->mock( 'pickup_locations', sub {
1261         return Koha::Libraries->new->empty;
1262     });
1263
1264     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1265       ->status_is(400)
1266       ->json_is({ error => 'The supplied pickup location is not valid' });
1267
1268     $schema->storage->txn_rollback;
1269 };
1270
1271 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1272
1273     plan tests => 28;
1274
1275     $schema->storage->txn_begin;
1276
1277     my $password = 'AbcdEFG123';
1278
1279     # library_1 and library_2 are available pickup locations, not library_3
1280     my $library_1 = $builder->build_object(
1281         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1282     my $library_2 = $builder->build_object(
1283         { class => 'Koha::Libraries', value => { pickup_location => 1 } } );
1284     my $library_3 = $builder->build_object(
1285         { class => 'Koha::Libraries', value => { pickup_location => 0 } } );
1286
1287     my $patron = $builder->build_object(
1288         { class => 'Koha::Patrons', value => { flags => 0 } } );
1289     $patron->set_password( { password => $password, skip_validation => 1 } );
1290     my $userid = $patron->userid;
1291     $builder->build(
1292         {
1293             source => 'UserPermission',
1294             value  => {
1295                 borrowernumber => $patron->borrowernumber,
1296                 module_bit     => 6,
1297                 code           => 'place_holds',
1298             },
1299         }
1300     );
1301
1302     # Disable logging
1303     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1304     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1305
1306     my $biblio = $builder->build_sample_biblio;
1307     my $item   = $builder->build_sample_item(
1308         {
1309             biblionumber => $biblio->biblionumber,
1310             library      => $library_1->branchcode
1311         }
1312     );
1313
1314     # biblio-level hold
1315     my $hold = Koha::Holds->find(
1316         AddReserve(
1317             {
1318                 branchcode     => $library_1->branchcode,
1319                 borrowernumber => $patron->borrowernumber,
1320                 biblionumber   => $biblio->biblionumber,
1321                 priority       => 1,
1322                 itemnumber     => undef,
1323             }
1324         )
1325     );
1326
1327     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1328           . $hold->id
1329           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1330       ->status_is(200)
1331       ->json_is({ pickup_library_id => $library_2->branchcode });
1332
1333     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1334
1335     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1336           . $hold->id
1337           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1338       ->status_is(400)
1339       ->json_is({ error => '[The supplied pickup location is not valid]' });
1340
1341     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1342
1343     # item-level hold
1344     $hold = Koha::Holds->find(
1345         AddReserve(
1346             {
1347                 branchcode     => $library_1->branchcode,
1348                 borrowernumber => $patron->borrowernumber,
1349                 biblionumber   => $biblio->biblionumber,
1350                 priority       => 1,
1351                 itemnumber     => $item->itemnumber,
1352             }
1353         )
1354     );
1355
1356     # Attempt to use an invalid pickup locations ends in 400
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_1->branchcode, 'pickup library unchanged' );
1364
1365     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
1366
1367     # Attempt to use an invalid pickup locations with override succeeds
1368     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1369           . $hold->id
1370           . "/pickup_location"
1371           => { 'x-koha-override' => 'any' }
1372           => json => { pickup_library_id => $library_2->branchcode } )
1373       ->status_is(200)
1374       ->json_is({ pickup_library_id => $library_2->branchcode });
1375
1376     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library changed' );
1377
1378     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
1379
1380     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1381           . $hold->id
1382           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1383       ->status_is(200)
1384       ->json_is({ pickup_library_id => $library_2->branchcode });
1385
1386     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1387
1388     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1389           . $hold->id
1390           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1391       ->status_is(400)
1392       ->json_is({ error => '[The supplied pickup location is not valid]' });
1393
1394     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used' );
1395
1396     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1397           . $hold->id
1398           . "/pickup_location"
1399           => { 'x-koha-override' => 'any' }
1400           => json => { pickup_library_id => $library_3->branchcode } )
1401       ->status_is(400)
1402       ->json_is({ error => '[The supplied pickup location is not valid]' });
1403
1404     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used, even if x-koha-override is passed' );
1405
1406     $schema->storage->txn_rollback;
1407 };
1408
1409 subtest 'delete() tests' => sub {
1410
1411     plan tests => 13;
1412
1413     $schema->storage->txn_begin;
1414
1415     my $password = 'AbcdEFG123';
1416     my $patron   = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 0 } } );
1417     $patron->set_password( { password => $password, skip_validation => 1 } );
1418     my $userid = $patron->userid;
1419
1420     # Only have 'place_holds' subpermission
1421     $builder->build(
1422         {
1423             source => 'UserPermission',
1424             value  => {
1425                 borrowernumber => $patron->borrowernumber,
1426                 module_bit     => 6,
1427                 code           => 'place_holds',
1428             },
1429         }
1430     );
1431
1432     # Disable logging
1433     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1434     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1435
1436     my $biblio = $builder->build_sample_biblio;
1437     my $item   = $builder->build_sample_item(
1438         {
1439             biblionumber => $biblio->biblionumber,
1440             library      => $patron->branchcode
1441         }
1442     );
1443
1444     # Add a hold
1445     my $hold = Koha::Holds->find(
1446         AddReserve(
1447             {
1448                 branchcode     => $patron->branchcode,
1449                 borrowernumber => $patron->borrowernumber,
1450                 biblionumber   => $biblio->biblionumber,
1451                 priority       => 1,
1452                 itemnumber     => undef,
1453             }
1454         )
1455     );
1456
1457     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id )->status_is( 204, 'SWAGGER3.2.4' )
1458         ->content_is( '', 'SWAGGER3.3.4' );
1459
1460     $hold = Koha::Holds->find(
1461         AddReserve(
1462             {
1463                 branchcode     => $patron->branchcode,
1464                 borrowernumber => $patron->borrowernumber,
1465                 biblionumber   => $biblio->biblionumber,
1466                 priority       => 1,
1467                 itemnumber     => undef,
1468             }
1469         )
1470     );
1471
1472     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id => { 'x-koha-override' => q{} } )
1473         ->status_is( 204, 'Same behavior if header not set' )->content_is('');
1474
1475     $hold = Koha::Holds->find(
1476         AddReserve(
1477             {
1478                 branchcode     => $patron->branchcode,
1479                 borrowernumber => $patron->borrowernumber,
1480                 biblionumber   => $biblio->biblionumber,
1481                 priority       => 1,
1482                 itemnumber     => undef,
1483             }
1484         )
1485     );
1486
1487     $t->delete_ok(
1488         "//$userid:$password@/api/v1/holds/" . $hold->id => { 'x-koha-override' => q{cancellation-request-flow} } )
1489         ->status_is( 204, 'Same behavior if header set but hold not waiting' )->content_is('');
1490
1491     $hold = Koha::Holds->find(
1492         AddReserve(
1493             {
1494                 branchcode     => $patron->branchcode,
1495                 borrowernumber => $patron->borrowernumber,
1496                 biblionumber   => $biblio->biblionumber,
1497                 priority       => 1,
1498                 itemnumber     => undef,
1499             }
1500         )
1501     );
1502
1503     $hold->set_waiting;
1504
1505     is( $hold->cancellation_requests->count, 0 );
1506
1507     $t->delete_ok(
1508         "//$userid:$password@/api/v1/holds/" . $hold->id => { 'x-koha-override' => q{cancellation-request-flow} } )
1509         ->status_is( 202, 'Cancellation request accepted' );
1510
1511     is( $hold->cancellation_requests->count, 1 );
1512
1513     $schema->storage->txn_rollback;
1514 };