Bug 27898: 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 => 13;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::TestBuilder;
24 use t::lib::Mocks;
25
26 use DateTime;
27 use Mojo::JSON qw(encode_json);
28
29 use C4::Context;
30 use Koha::Patrons;
31 use C4::Reserves;
32 use C4::Items;
33
34 use Koha::Database;
35 use Koha::DateUtils;
36 use Koha::Biblios;
37 use Koha::Biblioitems;
38 use Koha::Items;
39 use Koha::CirculationRules;
40
41 my $schema  = Koha::Database->new->schema;
42 my $builder = t::lib::TestBuilder->new();
43
44 $schema->storage->txn_begin;
45
46 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
47
48 my $t = Test::Mojo->new('Koha::REST::V1');
49
50 my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
51 my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
52 my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
53 my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype};
54
55 # Generic password for everyone
56 my $password = 'thePassword123';
57
58 # User without any permissions
59 my $nopermission = $builder->build_object({
60     class => 'Koha::Patrons',
61     value => {
62         branchcode   => $branchcode,
63         categorycode => $categorycode,
64         flags        => 0
65     }
66 });
67 $nopermission->set_password( { password => $password, skip_validation => 1 } );
68 my $nopermission_userid = $nopermission->userid;
69
70 my $patron_1 = $builder->build_object(
71     {
72         class => 'Koha::Patrons',
73         value => {
74             categorycode => $categorycode,
75             branchcode   => $branchcode,
76             surname      => 'Test Surname',
77             flags        => 80, #borrowers and reserveforothers flags
78         }
79     }
80 );
81 $patron_1->set_password( { password => $password, skip_validation => 1 } );
82 my $userid_1 = $patron_1->userid;
83
84 my $patron_2 = $builder->build_object(
85     {
86         class => 'Koha::Patrons',
87         value => {
88             categorycode => $categorycode,
89             branchcode   => $branchcode,
90             surname      => 'Test Surname 2',
91             flags        => 16, # borrowers flag
92         }
93     }
94 );
95 $patron_2->set_password( { password => $password, skip_validation => 1 } );
96 my $userid_2 = $patron_2->userid;
97
98 my $patron_3 = $builder->build_object(
99     {
100         class => 'Koha::Patrons',
101         value => {
102             categorycode => $categorycode,
103             branchcode   => $branchcode,
104             surname      => 'Test Surname 3',
105             flags        => 64, # reserveforothers flag
106         }
107     }
108 );
109 $patron_3->set_password( { password => $password, skip_validation => 1 } );
110 my $userid_3 = $patron_3->userid;
111
112 my $biblio_1 = $builder->build_sample_biblio;
113 my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber, itype => $itemtype });
114
115 my $biblio_2 = $builder->build_sample_biblio;
116 my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber, itype => $itemtype });
117
118 my $dbh = C4::Context->dbh;
119 $dbh->do('DELETE FROM reserves');
120 Koha::CirculationRules->search()->delete();
121 Koha::CirculationRules->set_rules(
122     {
123         categorycode => undef,
124         branchcode   => undef,
125         itemtype     => undef,
126         rules        => {
127             reservesallowed => 1,
128             holds_per_record => 99
129         }
130     }
131 );
132
133 my $reserve_id = C4::Reserves::AddReserve(
134     {
135         branchcode     => $branchcode,
136         borrowernumber => $patron_1->borrowernumber,
137         biblionumber   => $biblio_1->biblionumber,
138         priority       => 1,
139         itemnumber     => $item_1->itemnumber,
140     }
141 );
142
143 # Add another reserve to be able to change first reserve's rank
144 my $reserve_id2 = C4::Reserves::AddReserve(
145     {
146         branchcode     => $branchcode,
147         borrowernumber => $patron_2->borrowernumber,
148         biblionumber   => $biblio_1->biblionumber,
149         priority       => 2,
150         itemnumber     => $item_1->itemnumber,
151     }
152 );
153
154 my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' );
155 my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
156
157 my $post_data = {
158     patron_id => int($patron_1->borrowernumber),
159     biblio_id => int($biblio_1->biblionumber),
160     item_id => int($item_1->itemnumber),
161     pickup_library_id => $branchcode,
162     expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
163     priority => 2,
164 };
165 my $put_data = {
166     priority => 2,
167     suspended_until => output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }),
168 };
169
170 subtest "Test endpoints without authentication" => sub {
171     plan tests => 8;
172     $t->get_ok('/api/v1/holds')
173       ->status_is(401);
174     $t->post_ok('/api/v1/holds')
175       ->status_is(401);
176     $t->put_ok('/api/v1/holds/0')
177       ->status_is(401);
178     $t->delete_ok('/api/v1/holds/0')
179       ->status_is(401);
180 };
181
182 subtest "Test endpoints without permission" => sub {
183
184     plan tests => 10;
185
186     $t->get_ok( "//$nopermission_userid:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber ) # no permission
187       ->status_is(403);
188
189     $t->get_ok( "//$userid_3:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )    # no permission
190       ->status_is(403);
191
192     $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data )
193       ->status_is(403);
194
195     $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_data )
196       ->status_is(403);
197
198     $t->delete_ok( "//$nopermission_userid:$password@/api/v1/holds/0" )
199       ->status_is(403);
200 };
201
202 subtest "Test endpoints with permission" => sub {
203
204     plan tests => 44;
205
206     $t->get_ok( "//$userid_1:$password@/api/v1/holds" )
207       ->status_is(200)
208       ->json_has('/0')
209       ->json_has('/1')
210       ->json_hasnt('/2');
211
212     $t->get_ok( "//$userid_1:$password@/api/v1/holds?priority=2" )
213       ->status_is(200)
214       ->json_is('/0/patron_id', $patron_2->borrowernumber)
215       ->json_hasnt('/1');
216
217     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
218       ->status_is(204, 'SWAGGER3.2.4')
219       ->content_is('', 'SWAGGER3.3.4');
220
221     $t->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data )
222       ->status_is(404)
223       ->json_has('/error');
224
225     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
226       ->status_is(404)
227       ->json_has('/error');
228
229     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
230       ->status_is(200)
231       ->json_is([]);
232
233     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
234     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
235       ->status_is(200)
236       ->json_is([]);
237
238     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
239       ->status_is(204, 'SWAGGER3.2.4')
240       ->content_is('', 'SWAGGER3.3.4');
241
242     # Make sure pickup location checks doesn't get in the middle
243     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
244     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
245     my $mock_item   = Test::MockModule->new('Koha::Item');
246     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
247
248     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
249       ->status_is(201)
250       ->json_has('/hold_id');
251
252     # Get id from response
253     $reserve_id = $t->tx->res->json->{hold_id};
254
255     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
256       ->status_is(200)
257       ->json_is('/0/hold_id', $reserve_id)
258       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
259       ->json_is('/0/pickup_library_id', $branchcode);
260
261     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
262       ->status_is(403)
263       ->json_like('/error', qr/itemAlreadyOnHold/);
264
265     $post_data->{biblionumber} = int($biblio_2->biblionumber);
266     $post_data->{itemnumber}   = int($item_2->itemnumber);
267
268     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
269       ->status_is(403)
270       ->json_like('/error', qr/itemAlreadyOnHold/);
271
272     my $to_delete_patron  = $builder->build_object({ class => 'Koha::Patrons' });
273     my $deleted_patron_id = $to_delete_patron->borrowernumber;
274     $to_delete_patron->delete;
275
276     my $tmp_patron_id = $post_data->{patron_id};
277     $post_data->{patron_id} = $deleted_patron_id;
278     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
279       ->status_is(400)
280       ->json_is( { error => 'patron_id not found' } );
281
282     # Restore the original patron_id as it is expected by the next subtest
283     # FIXME: this tests need to be rewritten from scratch
284     $post_data->{patron_id} = $tmp_patron_id;
285 };
286
287 subtest 'Reserves with itemtype' => sub {
288     plan tests => 10;
289
290     my $post_data = {
291         patron_id => int($patron_1->borrowernumber),
292         biblio_id => int($biblio_1->biblionumber),
293         pickup_library_id => $branchcode,
294         item_type => $itemtype,
295     };
296
297     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
298       ->status_is(204, 'SWAGGER3.2.4')
299       ->content_is('', 'SWAGGER3.3.4');
300
301     # Make sure pickup location checks doesn't get in the middle
302     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
303     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
304     my $mock_item   = Test::MockModule->new('Koha::Item');
305     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
306
307     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
308       ->status_is(201)
309       ->json_has('/hold_id');
310
311     $reserve_id = $t->tx->res->json->{hold_id};
312
313     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
314       ->status_is(200)
315       ->json_is('/0/hold_id', $reserve_id)
316       ->json_is('/0/item_type', $itemtype);
317 };
318
319
320 subtest 'test AllowHoldDateInFuture' => sub {
321
322     plan tests => 6;
323
324     $dbh->do('DELETE FROM reserves');
325
326     my $future_hold_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
327
328     my $post_data = {
329         patron_id => int($patron_1->borrowernumber),
330         biblio_id => int($biblio_1->biblionumber),
331         item_id => int($item_1->itemnumber),
332         pickup_library_id => $branchcode,
333         expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
334         hold_date => output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }),
335         priority => 2,
336     };
337
338     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 );
339
340     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
341       ->status_is(400)
342       ->json_has('/error');
343
344     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 );
345
346     # Make sure pickup location checks doesn't get in the middle
347     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
348     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
349     my $mock_item   = Test::MockModule->new('Koha::Item');
350     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
351
352     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
353       ->status_is(201)
354       ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }));
355 };
356
357 $schema->storage->txn_rollback;
358
359 subtest 'x-koha-override and AllowHoldPolicyOverride tests' => sub {
360
361     plan tests => 16;
362
363     $schema->storage->txn_begin;
364
365     my $patron = $builder->build_object(
366         {
367             class => 'Koha::Patrons',
368             value => { flags => 1 }
369         }
370     );
371     my $password = 'thePassword123';
372     $patron->set_password( { password => $password, skip_validation => 1 } );
373     $patron->discard_changes;
374     my $userid = $patron->userid;
375
376     my $renegade_library = $builder->build_object({ class => 'Koha::Libraries' });
377
378     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
379
380     # Make sure pickup location checks doesn't get in the middle
381     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
382     $mock_biblio->mock( 'pickup_locations',
383         sub { return Koha::Libraries->search({ branchcode => { '!=' => $renegade_library->branchcode } }); } );
384     my $mock_item = Test::MockModule->new('Koha::Item');
385     $mock_item->mock( 'pickup_locations',
386         sub { return Koha::Libraries->search({ branchcode => { '!=' => $renegade_library->branchcode } }) } );
387
388     my $can_item_be_reserved_result;
389     my $mock_reserves = Test::MockModule->new('C4::Reserves');
390     $mock_reserves->mock(
391         'CanItemBeReserved',
392         sub {
393             return $can_item_be_reserved_result;
394         }
395     );
396
397     my $item = $builder->build_sample_item;
398
399     my $post_data = {
400         item_id           => $item->id,
401         biblio_id         => $item->biblionumber,
402         patron_id         => $patron->id,
403         pickup_library_id => $patron->branchcode,
404     };
405
406     $can_item_be_reserved_result = { status => 'ageRestricted' };
407
408     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
409       ->status_is(403)
410       ->json_is( '/error' => "Hold cannot be placed. Reason: ageRestricted" );
411
412     # x-koha-override doesn't override if AllowHoldPolicyOverride not set
413     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
414           { 'x-koha-override' => 'any' } => json => $post_data )
415       ->status_is(403);
416
417     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
418
419     $can_item_be_reserved_result = { status => 'pickupNotInHoldGroup' };
420
421     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
422       ->status_is(403)
423       ->json_is(
424         '/error' => "Hold cannot be placed. Reason: pickupNotInHoldGroup" );
425
426     # x-koha-override overrides the status
427     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
428           { 'x-koha-override' => 'any' } => json => $post_data )
429       ->status_is(201);
430
431     $can_item_be_reserved_result = { status => 'OK' };
432
433     # x-koha-override works when status not need override
434     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
435           { 'x-koha-override' => 'any' } => json => $post_data )
436       ->status_is(201);
437
438     # Test pickup locations can be overridden
439     $post_data->{pickup_library_id} = $renegade_library->branchcode;
440
441     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
442       ->status_is(400);
443
444     $t->post_ok( "//$userid:$password@/api/v1/holds" =>
445           { 'x-koha-override' => 'any' } => json => $post_data )
446       ->status_is(201);
447
448     $schema->storage->txn_rollback;
449 };
450
451 subtest 'suspend and resume tests' => sub {
452
453     plan tests => 24;
454
455     $schema->storage->txn_begin;
456
457     my $password = 'AbcdEFG123';
458
459     my $patron = $builder->build_object(
460         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
461     $patron->set_password({ password => $password, skip_validation => 1 });
462     my $userid = $patron->userid;
463
464     # Disable logging
465     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
466     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
467
468     my $hold = $builder->build_object(
469         {   class => 'Koha::Holds',
470             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
471         }
472     );
473
474     ok( !$hold->is_suspended, 'Hold is not suspended' );
475     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
476         ->status_is( 201, 'Hold suspension created' );
477
478     $hold->discard_changes;    # refresh object
479
480     ok( $hold->is_suspended, 'Hold is suspended' );
481     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
482
483     my $end_date = output_pref({
484       dt         => dt_from_string( undef ),
485       dateformat => 'rfc3339',
486       dateonly   => 1
487     });
488
489     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
490
491     $hold->discard_changes;    # refresh object
492
493     ok( $hold->is_suspended, 'Hold is suspended' );
494     $t->json_is(
495       '/end_date',
496       output_pref({
497         dt         => dt_from_string( $hold->suspend_until ),
498         dateformat => 'rfc3339',
499         dateonly   => 1
500       }),
501       'Hold suspension has correct end date'
502     );
503
504     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
505       ->status_is(204, 'SWAGGER3.2.4')
506       ->content_is('', 'SWAGGER3.3.4');
507
508     # Pass a an expiration date for the suspension
509     my $date = dt_from_string()->add( days => 5 );
510     $t->post_ok(
511               "//$userid:$password@/api/v1/holds/"
512             . $hold->id
513             . "/suspension" => json => {
514             end_date =>
515                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
516             }
517     )->status_is( 201, 'Hold suspension created' )
518         ->json_is( '/end_date',
519         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
520         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
521
522     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
523       ->status_is(204, 'SWAGGER3.2.4')
524       ->content_is('', 'SWAGGER3.3.4');
525
526     $hold->set_waiting->discard_changes;
527
528     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
529       ->status_is( 400, 'Cannot suspend waiting hold' )
530       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
531
532     $hold->set_transfer->discard_changes;
533
534     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
535       ->status_is( 400, 'Cannot suspend hold on transfer' )
536       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
537
538     $schema->storage->txn_rollback;
539 };
540
541 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
542
543     plan tests => 14;
544
545     $schema->storage->txn_begin;
546
547     my $password = 'AbcdEFG123';
548
549     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
550     my $patron_np = $builder->build_object(
551         { class => 'Koha::Patrons', value => { flags => 0 } } );
552     $patron_np->set_password( { password => $password, skip_validation => 1 } );
553     my $userid_np = $patron_np->userid;
554
555     my $patron = $builder->build_object(
556         { class => 'Koha::Patrons', value => { flags => 0 } } );
557     $patron->set_password( { password => $password, skip_validation => 1 } );
558     my $userid = $patron->userid;
559     $builder->build(
560         {
561             source => 'UserPermission',
562             value  => {
563                 borrowernumber => $patron->borrowernumber,
564                 module_bit     => 6,
565                 code           => 'modify_holds_priority',
566             },
567         }
568     );
569
570     # Disable logging
571     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
572     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
573
574     my $biblio   = $builder->build_sample_biblio;
575     my $patron_1 = $builder->build_object(
576         {
577             class => 'Koha::Patrons',
578             value => { branchcode => $library->branchcode }
579         }
580     );
581     my $patron_2 = $builder->build_object(
582         {
583             class => 'Koha::Patrons',
584             value => { branchcode => $library->branchcode }
585         }
586     );
587     my $patron_3 = $builder->build_object(
588         {
589             class => 'Koha::Patrons',
590             value => { branchcode => $library->branchcode }
591         }
592     );
593
594     my $hold_1 = Koha::Holds->find(
595         AddReserve(
596             {
597                 branchcode     => $library->branchcode,
598                 borrowernumber => $patron_1->borrowernumber,
599                 biblionumber   => $biblio->biblionumber,
600                 priority       => 1,
601             }
602         )
603     );
604     my $hold_2 = Koha::Holds->find(
605         AddReserve(
606             {
607                 branchcode     => $library->branchcode,
608                 borrowernumber => $patron_2->borrowernumber,
609                 biblionumber   => $biblio->biblionumber,
610                 priority       => 2,
611             }
612         )
613     );
614     my $hold_3 = Koha::Holds->find(
615         AddReserve(
616             {
617                 branchcode     => $library->branchcode,
618                 borrowernumber => $patron_3->borrowernumber,
619                 biblionumber   => $biblio->biblionumber,
620                 priority       => 3,
621             }
622         )
623     );
624
625     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
626           . $hold_3->id
627           . "/priority" => json => 1 )->status_is(403);
628
629     $t->put_ok( "//$userid:$password@/api/v1/holds/"
630           . $hold_3->id
631           . "/priority" => json => 1 )->status_is(200)->json_is(1);
632
633     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
634     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
635     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
636
637     $t->put_ok( "//$userid:$password@/api/v1/holds/"
638           . $hold_3->id
639           . "/priority" => json => 3 )->status_is(200)->json_is(3);
640
641     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
642     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
643     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
644
645     $schema->storage->txn_rollback;
646 };
647
648 subtest 'add() tests (maxreserves behaviour)' => sub {
649
650     plan tests => 7;
651
652     $schema->storage->txn_begin;
653
654     $dbh->do('DELETE FROM reserves');
655
656     Koha::CirculationRules->new->delete;
657
658     my $password = 'AbcdEFG123';
659
660     my $patron = $builder->build_object(
661         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
662     $patron->set_password({ password => $password, skip_validation => 1 });
663     my $userid = $patron->userid;
664
665     Koha::CirculationRules->set_rules(
666         {
667             itemtype     => undef,
668             branchcode   => undef,
669             categorycode => undef,
670             rules        => {
671                 reservesallowed => 3
672             }
673         }
674     );
675
676     Koha::CirculationRules->set_rules(
677         {
678             branchcode   => undef,
679             categorycode => $patron->categorycode,
680             rules        => {
681                 max_holds   => 4,
682             }
683         }
684     );
685
686     my $biblio_1 = $builder->build_sample_biblio;
687     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
688     my $biblio_2 = $builder->build_sample_biblio;
689     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
690     my $biblio_3 = $builder->build_sample_biblio;
691     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
692
693     # Make sure pickup location checks doesn't get in the middle
694     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
695     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
696     my $mock_item   = Test::MockModule->new('Koha::Item');
697     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
698
699     # Disable logging
700     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
701     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
702     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
703     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
704
705     my $post_data = {
706         patron_id => $patron->borrowernumber,
707         biblio_id => $biblio_1->biblionumber,
708         pickup_library_id => $item_1->home_branch->branchcode,
709         item_type => $item_1->itype,
710     };
711
712     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
713       ->status_is(201);
714
715     $post_data = {
716         patron_id => $patron->borrowernumber,
717         biblio_id => $biblio_2->biblionumber,
718         pickup_library_id => $item_2->home_branch->branchcode,
719         item_id   => $item_2->itemnumber
720     };
721
722     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
723       ->status_is(201);
724
725     $post_data = {
726         patron_id => $patron->borrowernumber,
727         biblio_id => $biblio_3->biblionumber,
728         pickup_library_id => $item_1->home_branch->branchcode,
729         item_id   => $item_3->itemnumber
730     };
731
732     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
733       ->status_is(403)
734       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
735
736     $schema->storage->txn_rollback;
737 };
738
739 subtest 'pickup_locations() tests' => sub {
740
741     plan tests => 12;
742
743     $schema->storage->txn_begin;
744
745     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
746
747     # Small trick to ease testing
748     Koha::Libraries->search->update({ pickup_location => 0 });
749
750     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } });
751     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } });
752     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } });
753
754     my $library_1_api = $library_1->to_api();
755     my $library_2_api = $library_2->to_api();
756     my $library_3_api = $library_3->to_api();
757
758     $library_1_api->{needs_override} = Mojo::JSON->false;
759     $library_2_api->{needs_override} = Mojo::JSON->false;
760     $library_3_api->{needs_override} = Mojo::JSON->false;
761
762     my $patron = $builder->build_object(
763         {
764             class => 'Koha::Patrons',
765             value => { userid => 'tomasito', flags => 0 }
766         }
767     );
768     $patron->set_password( { password => $password, skip_validation => 1 } );
769     my $userid = $patron->userid;
770     $builder->build(
771         {
772             source => 'UserPermission',
773             value  => {
774                 borrowernumber => $patron->borrowernumber,
775                 module_bit     => 6,
776                 code           => 'place_holds',
777             },
778         }
779     );
780
781     my $item_class = Test::MockModule->new('Koha::Item');
782     $item_class->mock(
783         'pickup_locations',
784         sub {
785             my ( $self, $params ) = @_;
786             my $mock_patron = $params->{patron};
787             is( $mock_patron->borrowernumber,
788                 $patron->borrowernumber, 'Patron passed correctly' );
789             return Koha::Libraries->search(
790                 {
791                     branchcode => {
792                         '-in' => [
793                             $library_1->branchcode,
794                             $library_2->branchcode
795                         ]
796                     }
797                 },
798                 {   # we make sure no surprises in the order of the result
799                     order_by => { '-asc' => 'marcorgcode' }
800                 }
801             );
802         }
803     );
804
805     my $biblio_class = Test::MockModule->new('Koha::Biblio');
806     $biblio_class->mock(
807         'pickup_locations',
808         sub {
809             my ( $self, $params ) = @_;
810             my $mock_patron = $params->{patron};
811             is( $mock_patron->borrowernumber,
812                 $patron->borrowernumber, 'Patron passed correctly' );
813             return Koha::Libraries->search(
814                 {
815                     branchcode => {
816                         '-in' => [
817                             $library_2->branchcode,
818                             $library_3->branchcode
819                         ]
820                     }
821                 },
822                 {   # we make sure no surprises in the order of the result
823                     order_by => { '-asc' => 'marcorgcode' }
824                 }
825             );
826         }
827     );
828
829     my $item = $builder->build_sample_item;
830
831     # biblio-level hold
832     my $hold_1 = $builder->build_object(
833         {
834             class => 'Koha::Holds',
835             value => {
836                 itemnumber     => undef,
837                 biblionumber   => $item->biblionumber,
838                 borrowernumber => $patron->borrowernumber
839             }
840         }
841     );
842     # item-level hold
843     my $hold_2 = $builder->build_object(
844         {
845             class => 'Koha::Holds',
846             value => {
847                 itemnumber     => $item->itemnumber,
848                 biblionumber   => $item->biblionumber,
849                 borrowernumber => $patron->borrowernumber
850             }
851         }
852     );
853
854     $t->get_ok( "//$userid:$password@/api/v1/holds/"
855           . $hold_1->id
856           . "/pickup_locations" )
857       ->json_is( [ $library_2_api, $library_3_api ] );
858
859     $t->get_ok( "//$userid:$password@/api/v1/holds/"
860           . $hold_2->id
861           . "/pickup_locations" )
862       ->json_is( [ $library_1_api, $library_2_api ] );
863
864     # filtering works!
865     $t->get_ok( "//$userid:$password@/api/v1/holds/"
866           . $hold_2->id
867           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
868       ->json_is( [ $library_1_api ] );
869
870     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
871
872     my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } });
873     my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } });
874
875     my $library_5_api = $library_5->to_api();
876     $library_5_api->{needs_override} = Mojo::JSON->true;
877
878     # bibli-level mock doesn't include library_1 as valid pickup location
879     $library_1_api->{needs_override} = Mojo::JSON->true;
880
881     $t->get_ok( "//$userid:$password@/api/v1/holds/"
882           . $hold_1->id
883           . "/pickup_locations?_order_by=marc_org_code" )
884       ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] );
885
886     $schema->storage->txn_rollback;
887 };
888
889 subtest 'edit() tests' => sub {
890
891     plan tests => 20;
892
893     $schema->storage->txn_begin;
894
895     my $password = 'AbcdEFG123';
896
897     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
898     my $patron = $builder->build_object(
899         { class => 'Koha::Patrons', value => { flags => 1 } } );
900     $patron->set_password( { password => $password, skip_validation => 1 } );
901     my $userid = $patron->userid;
902     $builder->build(
903         {
904             source => 'UserPermission',
905             value  => {
906                 borrowernumber => $patron->borrowernumber,
907                 module_bit     => 6,
908                 code           => 'modify_holds_priority',
909             },
910         }
911     );
912
913     # Disable logging
914     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
915     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
916     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
917
918     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
919     my $mock_item   = Test::MockModule->new('Koha::Item');
920
921     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
922     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
923     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
924
925     # let's control what Koha::Biblio->pickup_locations returns, for testing
926     $mock_biblio->mock( 'pickup_locations', sub {
927         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
928     });
929     # let's mock what Koha::Item->pickup_locations returns, for testing
930     $mock_item->mock( 'pickup_locations', sub {
931         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
932     });
933
934     my $biblio = $builder->build_sample_biblio;
935     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
936
937     # Test biblio-level holds
938     my $biblio_hold = $builder->build_object(
939         {
940             class => "Koha::Holds",
941             value => {
942                 biblionumber => $biblio->biblionumber,
943                 branchcode   => $library_3->branchcode,
944                 itemnumber   => undef,
945                 priority     => 1,
946             }
947         }
948     );
949
950     my $biblio_hold_data = $biblio_hold->to_api;
951     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
952
953     $t->put_ok( "//$userid:$password@/api/v1/holds/"
954           . $biblio_hold->id
955           => json => $biblio_hold_data )
956       ->status_is(400)
957       ->json_is({ error => 'The supplied pickup location is not valid' });
958
959     $biblio_hold->discard_changes;
960     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
961
962     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id
963           => { 'x-koha-override' => 'any' }
964           => json => $biblio_hold_data )
965       ->status_is(200)
966       ->json_has( '/pickup_library_id' => $library_1->id );
967
968     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
969     $t->put_ok( "//$userid:$password@/api/v1/holds/"
970           . $biblio_hold->id
971           => json => $biblio_hold_data )
972       ->status_is(200);
973
974     $biblio_hold->discard_changes;
975     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
976
977     # Test item-level holds
978     my $item_hold = $builder->build_object(
979         {
980             class => "Koha::Holds",
981             value => {
982                 biblionumber => $biblio->biblionumber,
983                 branchcode   => $library_3->branchcode,
984                 itemnumber   => $item->itemnumber,
985                 priority     => 1,
986             }
987         }
988     );
989
990     my $item_hold_data = $item_hold->to_api;
991     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
992
993     $t->put_ok( "//$userid:$password@/api/v1/holds/"
994           . $item_hold->id
995           => json => $item_hold_data )
996       ->status_is(400)
997       ->json_is({ error => 'The supplied pickup location is not valid' });
998
999     $item_hold->discard_changes;
1000     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
1001
1002     $t->put_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id
1003           => { 'x-koha-override' => 'any' }
1004           => json => $item_hold_data )
1005       ->status_is(200)
1006       ->json_has( '/pickup_library_id' => $library_1->id );
1007
1008     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1009     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1010           . $item_hold->id
1011           => json => $item_hold_data )
1012       ->status_is(200);
1013
1014     $item_hold->discard_changes;
1015     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
1016
1017     $schema->storage->txn_rollback;
1018 };
1019
1020 subtest 'add() tests' => sub {
1021
1022     plan tests => 10;
1023
1024     $schema->storage->txn_begin;
1025
1026     my $password = 'AbcdEFG123';
1027
1028     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
1029     my $patron = $builder->build_object(
1030         { class => 'Koha::Patrons', value => { flags => 1 } } );
1031     $patron->set_password( { password => $password, skip_validation => 1 } );
1032     my $userid = $patron->userid;
1033     $builder->build(
1034         {
1035             source => 'UserPermission',
1036             value  => {
1037                 borrowernumber => $patron->borrowernumber,
1038                 module_bit     => 6,
1039                 code           => 'modify_holds_priority',
1040             },
1041         }
1042     );
1043
1044     # Disable logging
1045     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1046     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1047
1048     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
1049     my $mock_item   = Test::MockModule->new('Koha::Item');
1050
1051     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1052     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1053     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1054
1055     # let's control what Koha::Biblio->pickup_locations returns, for testing
1056     $mock_biblio->mock( 'pickup_locations', sub {
1057         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1058     });
1059     # let's mock what Koha::Item->pickup_locations returns, for testing
1060     $mock_item->mock( 'pickup_locations', sub {
1061         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
1062     });
1063
1064     my $can_be_reserved = 'OK';
1065     my $mock_reserves = Test::MockModule->new('C4::Reserves');
1066     $mock_reserves->mock( 'CanItemBeReserved', sub
1067         {
1068             return { status => $can_be_reserved }
1069         }
1070
1071     );
1072     $mock_reserves->mock( 'CanBookBeReserved', sub
1073         {
1074             return { status => $can_be_reserved }
1075         }
1076
1077     );
1078
1079     my $biblio = $builder->build_sample_biblio;
1080     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1081
1082     # Test biblio-level holds
1083     my $biblio_hold = $builder->build_object(
1084         {
1085             class => "Koha::Holds",
1086             value => {
1087                 biblionumber => $biblio->biblionumber,
1088                 branchcode   => $library_3->branchcode,
1089                 itemnumber   => undef,
1090                 priority     => 1,
1091             }
1092         }
1093     );
1094
1095     my $biblio_hold_data = $biblio_hold->to_api;
1096     $biblio_hold->delete;
1097     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
1098     delete $biblio_hold_data->{hold_id};
1099
1100     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1101       ->status_is(400)
1102       ->json_is({ error => 'The supplied pickup location is not valid' });
1103
1104     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1105     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1106       ->status_is(201);
1107
1108     # Test item-level holds
1109     my $item_hold = $builder->build_object(
1110         {
1111             class => "Koha::Holds",
1112             value => {
1113                 biblionumber => $biblio->biblionumber,
1114                 branchcode   => $library_3->branchcode,
1115                 itemnumber   => $item->itemnumber,
1116                 priority     => 1,
1117             }
1118         }
1119     );
1120
1121     my $item_hold_data = $item_hold->to_api;
1122     $item_hold->delete;
1123     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
1124     delete $item_hold->{hold_id};
1125
1126     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1127       ->status_is(400)
1128       ->json_is({ error => 'The supplied pickup location is not valid' });
1129
1130     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1131     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1132       ->status_is(201);
1133
1134     $schema->storage->txn_rollback;
1135 };
1136
1137 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1138
1139     plan tests => 16;
1140
1141     $schema->storage->txn_begin;
1142
1143     my $password = 'AbcdEFG123';
1144
1145     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
1146     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1147     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
1148
1149     my $patron = $builder->build_object(
1150         { class => 'Koha::Patrons', value => { flags => 0 } } );
1151     $patron->set_password( { password => $password, skip_validation => 1 } );
1152     my $userid = $patron->userid;
1153     $builder->build(
1154         {
1155             source => 'UserPermission',
1156             value  => {
1157                 borrowernumber => $patron->borrowernumber,
1158                 module_bit     => 6,
1159                 code           => 'place_holds',
1160             },
1161         }
1162     );
1163
1164     # Disable logging
1165     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1166     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1167
1168     my $libraries_query = { branchcode => [ $library_1->branchcode, $library_2->branchcode ] };
1169
1170     my $mocked_biblio = Test::MockModule->new('Koha::Biblio');
1171     $mocked_biblio->mock( 'pickup_locations', sub {
1172         return Koha::Libraries->search($libraries_query);
1173     });
1174
1175     my $mocked_item = Test::MockModule->new('Koha::Item');
1176     $mocked_item->mock( 'pickup_locations', sub {
1177         return Koha::Libraries->search($libraries_query);
1178     });
1179
1180     my $biblio = $builder->build_sample_biblio;
1181     my $item   = $builder->build_sample_item(
1182         {
1183             biblionumber => $biblio->biblionumber,
1184             library      => $library_1->branchcode
1185         }
1186     );
1187
1188     # biblio-level hold
1189     my $hold = Koha::Holds->find(
1190         AddReserve(
1191             {
1192                 branchcode     => $library_1->branchcode,
1193                 borrowernumber => $patron->borrowernumber,
1194                 biblionumber   => $biblio->biblionumber,
1195                 priority       => 1,
1196                 itemnumber     => undef,
1197             }
1198         )
1199     );
1200
1201     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1202           . $hold->id
1203           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1204       ->status_is(200)
1205       ->json_is({ pickup_library_id => $library_2->branchcode });
1206
1207     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1208
1209     $libraries_query = { branchcode => $library_1->branchcode };
1210
1211     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1212           . $hold->id
1213           . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } )
1214       ->status_is(400)
1215       ->json_is({ error => '[The supplied pickup location is not valid]' });
1216
1217     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' );
1218
1219     # item-level hold
1220     $hold = Koha::Holds->find(
1221         AddReserve(
1222             {
1223                 branchcode     => $library_1->branchcode,
1224                 borrowernumber => $patron->borrowernumber,
1225                 biblionumber   => $biblio->biblionumber,
1226                 priority       => 1,
1227                 itemnumber     => $item->itemnumber,
1228             }
1229         )
1230     );
1231
1232     $libraries_query = { branchcode => $library_1->branchcode };
1233
1234     # Attempt to use an invalid pickup locations ends in 400
1235     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1236           . $hold->id
1237           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1238       ->status_is(400)
1239       ->json_is({ error => '[The supplied pickup location is not valid]' });
1240
1241     is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' );
1242
1243     $libraries_query = {
1244         branchcode => {
1245             '-in' => [ $library_1->branchcode, $library_2->branchcode ]
1246         }
1247     };
1248
1249     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1250           . $hold->id
1251           . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } )
1252       ->status_is(200)
1253       ->json_is({ pickup_library_id => $library_2->branchcode });
1254
1255     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1256
1257     $schema->storage->txn_rollback;
1258 };