Bug 18729: Add PUT /holds/{hold_id}/pickup_location
[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 subtest 'test AllowHoldPolicyOverride' => sub {
358
359     plan tests => 7;
360
361     $dbh->do('DELETE FROM reserves');
362
363     Koha::CirculationRules->set_rules(
364         {
365             itemtype   => undef,
366             branchcode => undef,
367             rules      => {
368                 holdallowed => 'from_home_library'
369             }
370         }
371     );
372
373     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
374
375     # Make sure pickup location checks doesn't get in the middle
376     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
377     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
378     my $mock_item   = Test::MockModule->new('Koha::Item');
379     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
380
381     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
382       ->status_is(403)
383       ->json_has('/error');
384
385     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
386
387     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
388       ->status_is(403);
389
390     $t->post_ok(
391         "//$userid_3:$password@/api/v1/holds" => {
392             'x-koha-override' =>
393               encode_json( { AllowHoldPolicyOverride => Mojo::JSON->true } )
394         } => json => $post_data
395     )->status_is(201);
396 };
397
398 $schema->storage->txn_rollback;
399
400 subtest 'suspend and resume tests' => sub {
401
402     plan tests => 24;
403
404     $schema->storage->txn_begin;
405
406     my $password = 'AbcdEFG123';
407
408     my $patron = $builder->build_object(
409         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
410     $patron->set_password({ password => $password, skip_validation => 1 });
411     my $userid = $patron->userid;
412
413     # Disable logging
414     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
415     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
416
417     my $hold = $builder->build_object(
418         {   class => 'Koha::Holds',
419             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
420         }
421     );
422
423     ok( !$hold->is_suspended, 'Hold is not suspended' );
424     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
425         ->status_is( 201, 'Hold suspension created' );
426
427     $hold->discard_changes;    # refresh object
428
429     ok( $hold->is_suspended, 'Hold is suspended' );
430     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
431
432     my $end_date = output_pref({
433       dt         => dt_from_string( undef ),
434       dateformat => 'rfc3339',
435       dateonly   => 1
436     });
437
438     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
439
440     $hold->discard_changes;    # refresh object
441
442     ok( $hold->is_suspended, 'Hold is suspended' );
443     $t->json_is(
444       '/end_date',
445       output_pref({
446         dt         => dt_from_string( $hold->suspend_until ),
447         dateformat => 'rfc3339',
448         dateonly   => 1
449       }),
450       'Hold suspension has correct end date'
451     );
452
453     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
454       ->status_is(204, 'SWAGGER3.2.4')
455       ->content_is('', 'SWAGGER3.3.4');
456
457     # Pass a an expiration date for the suspension
458     my $date = dt_from_string()->add( days => 5 );
459     $t->post_ok(
460               "//$userid:$password@/api/v1/holds/"
461             . $hold->id
462             . "/suspension" => json => {
463             end_date =>
464                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
465             }
466     )->status_is( 201, 'Hold suspension created' )
467         ->json_is( '/end_date',
468         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
469         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
470
471     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
472       ->status_is(204, 'SWAGGER3.2.4')
473       ->content_is('', 'SWAGGER3.3.4');
474
475     $hold->set_waiting->discard_changes;
476
477     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
478       ->status_is( 400, 'Cannot suspend waiting hold' )
479       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
480
481     $hold->set_transfer->discard_changes;
482
483     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
484       ->status_is( 400, 'Cannot suspend hold on transfer' )
485       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
486
487     $schema->storage->txn_rollback;
488 };
489
490 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
491
492     plan tests => 14;
493
494     $schema->storage->txn_begin;
495
496     my $password = 'AbcdEFG123';
497
498     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
499     my $patron_np = $builder->build_object(
500         { class => 'Koha::Patrons', value => { flags => 0 } } );
501     $patron_np->set_password( { password => $password, skip_validation => 1 } );
502     my $userid_np = $patron_np->userid;
503
504     my $patron = $builder->build_object(
505         { class => 'Koha::Patrons', value => { flags => 0 } } );
506     $patron->set_password( { password => $password, skip_validation => 1 } );
507     my $userid = $patron->userid;
508     $builder->build(
509         {
510             source => 'UserPermission',
511             value  => {
512                 borrowernumber => $patron->borrowernumber,
513                 module_bit     => 6,
514                 code           => 'modify_holds_priority',
515             },
516         }
517     );
518
519     # Disable logging
520     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
521     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
522
523     my $biblio   = $builder->build_sample_biblio;
524     my $patron_1 = $builder->build_object(
525         {
526             class => 'Koha::Patrons',
527             value => { branchcode => $library->branchcode }
528         }
529     );
530     my $patron_2 = $builder->build_object(
531         {
532             class => 'Koha::Patrons',
533             value => { branchcode => $library->branchcode }
534         }
535     );
536     my $patron_3 = $builder->build_object(
537         {
538             class => 'Koha::Patrons',
539             value => { branchcode => $library->branchcode }
540         }
541     );
542
543     my $hold_1 = Koha::Holds->find(
544         AddReserve(
545             {
546                 branchcode     => $library->branchcode,
547                 borrowernumber => $patron_1->borrowernumber,
548                 biblionumber   => $biblio->biblionumber,
549                 priority       => 1,
550             }
551         )
552     );
553     my $hold_2 = Koha::Holds->find(
554         AddReserve(
555             {
556                 branchcode     => $library->branchcode,
557                 borrowernumber => $patron_2->borrowernumber,
558                 biblionumber   => $biblio->biblionumber,
559                 priority       => 2,
560             }
561         )
562     );
563     my $hold_3 = Koha::Holds->find(
564         AddReserve(
565             {
566                 branchcode     => $library->branchcode,
567                 borrowernumber => $patron_3->borrowernumber,
568                 biblionumber   => $biblio->biblionumber,
569                 priority       => 3,
570             }
571         )
572     );
573
574     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
575           . $hold_3->id
576           . "/priority" => json => 1 )->status_is(403);
577
578     $t->put_ok( "//$userid:$password@/api/v1/holds/"
579           . $hold_3->id
580           . "/priority" => json => 1 )->status_is(200)->json_is(1);
581
582     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
583     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
584     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
585
586     $t->put_ok( "//$userid:$password@/api/v1/holds/"
587           . $hold_3->id
588           . "/priority" => json => 3 )->status_is(200)->json_is(3);
589
590     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
591     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
592     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
593
594     $schema->storage->txn_rollback;
595 };
596
597 subtest 'add() tests (maxreserves behaviour)' => sub {
598
599     plan tests => 7;
600
601     $schema->storage->txn_begin;
602
603     $dbh->do('DELETE FROM reserves');
604
605     Koha::CirculationRules->new->delete;
606
607     my $password = 'AbcdEFG123';
608
609     my $patron = $builder->build_object(
610         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
611     $patron->set_password({ password => $password, skip_validation => 1 });
612     my $userid = $patron->userid;
613
614     Koha::CirculationRules->set_rules(
615         {
616             itemtype     => undef,
617             branchcode   => undef,
618             categorycode => undef,
619             rules        => {
620                 reservesallowed => 3
621             }
622         }
623     );
624
625     Koha::CirculationRules->set_rules(
626         {
627             branchcode   => undef,
628             categorycode => $patron->categorycode,
629             rules        => {
630                 max_holds   => 4,
631             }
632         }
633     );
634
635     my $biblio_1 = $builder->build_sample_biblio;
636     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
637     my $biblio_2 = $builder->build_sample_biblio;
638     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
639     my $biblio_3 = $builder->build_sample_biblio;
640     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
641
642     # Make sure pickup location checks doesn't get in the middle
643     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
644     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
645     my $mock_item   = Test::MockModule->new('Koha::Item');
646     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
647
648     # Disable logging
649     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
650     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
651     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
652     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
653
654     my $post_data = {
655         patron_id => $patron->borrowernumber,
656         biblio_id => $biblio_1->biblionumber,
657         pickup_library_id => $item_1->home_branch->branchcode,
658         item_type => $item_1->itype,
659     };
660
661     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
662       ->status_is(201);
663
664     $post_data = {
665         patron_id => $patron->borrowernumber,
666         biblio_id => $biblio_2->biblionumber,
667         pickup_library_id => $item_2->home_branch->branchcode,
668         item_id   => $item_2->itemnumber
669     };
670
671     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
672       ->status_is(201);
673
674     $post_data = {
675         patron_id => $patron->borrowernumber,
676         biblio_id => $biblio_3->biblionumber,
677         pickup_library_id => $item_1->home_branch->branchcode,
678         item_id   => $item_3->itemnumber
679     };
680
681     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
682       ->status_is(403)
683       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
684
685     $schema->storage->txn_rollback;
686 };
687
688 subtest 'pickup_locations() tests' => sub {
689
690     plan tests => 12;
691
692     $schema->storage->txn_begin;
693
694     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
695
696     # Small trick to ease testing
697     Koha::Libraries->search->update({ pickup_location => 0 });
698
699     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } });
700     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } });
701     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } });
702
703     my $library_1_api = $library_1->to_api();
704     my $library_2_api = $library_2->to_api();
705     my $library_3_api = $library_3->to_api();
706
707     $library_1_api->{needs_override} = Mojo::JSON->false;
708     $library_2_api->{needs_override} = Mojo::JSON->false;
709     $library_3_api->{needs_override} = Mojo::JSON->false;
710
711     my $patron = $builder->build_object(
712         {
713             class => 'Koha::Patrons',
714             value => { userid => 'tomasito', flags => 0 }
715         }
716     );
717     $patron->set_password( { password => $password, skip_validation => 1 } );
718     my $userid = $patron->userid;
719     $builder->build(
720         {
721             source => 'UserPermission',
722             value  => {
723                 borrowernumber => $patron->borrowernumber,
724                 module_bit     => 6,
725                 code           => 'place_holds',
726             },
727         }
728     );
729
730     my $item_class = Test::MockModule->new('Koha::Item');
731     $item_class->mock(
732         'pickup_locations',
733         sub {
734             my ( $self, $params ) = @_;
735             my $mock_patron = $params->{patron};
736             is( $mock_patron->borrowernumber,
737                 $patron->borrowernumber, 'Patron passed correctly' );
738             return Koha::Libraries->search(
739                 {
740                     branchcode => {
741                         '-in' => [
742                             $library_1->branchcode,
743                             $library_2->branchcode
744                         ]
745                     }
746                 },
747                 {   # we make sure no surprises in the order of the result
748                     order_by => { '-asc' => 'marcorgcode' }
749                 }
750             );
751         }
752     );
753
754     my $biblio_class = Test::MockModule->new('Koha::Biblio');
755     $biblio_class->mock(
756         'pickup_locations',
757         sub {
758             my ( $self, $params ) = @_;
759             my $mock_patron = $params->{patron};
760             is( $mock_patron->borrowernumber,
761                 $patron->borrowernumber, 'Patron passed correctly' );
762             return Koha::Libraries->search(
763                 {
764                     branchcode => {
765                         '-in' => [
766                             $library_2->branchcode,
767                             $library_3->branchcode
768                         ]
769                     }
770                 },
771                 {   # we make sure no surprises in the order of the result
772                     order_by => { '-asc' => 'marcorgcode' }
773                 }
774             );
775         }
776     );
777
778     my $item = $builder->build_sample_item;
779
780     # biblio-level hold
781     my $hold_1 = $builder->build_object(
782         {
783             class => 'Koha::Holds',
784             value => {
785                 itemnumber     => undef,
786                 biblionumber   => $item->biblionumber,
787                 borrowernumber => $patron->borrowernumber
788             }
789         }
790     );
791     # item-level hold
792     my $hold_2 = $builder->build_object(
793         {
794             class => 'Koha::Holds',
795             value => {
796                 itemnumber     => $item->itemnumber,
797                 biblionumber   => $item->biblionumber,
798                 borrowernumber => $patron->borrowernumber
799             }
800         }
801     );
802
803     $t->get_ok( "//$userid:$password@/api/v1/holds/"
804           . $hold_1->id
805           . "/pickup_locations" )
806       ->json_is( [ $library_2_api, $library_3_api ] );
807
808     $t->get_ok( "//$userid:$password@/api/v1/holds/"
809           . $hold_2->id
810           . "/pickup_locations" )
811       ->json_is( [ $library_1_api, $library_2_api ] );
812
813     # filtering works!
814     $t->get_ok( "//$userid:$password@/api/v1/holds/"
815           . $hold_2->id
816           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
817       ->json_is( [ $library_1_api ] );
818
819     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
820
821     my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } });
822     my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } });
823
824     my $library_5_api = $library_5->to_api();
825     $library_5_api->{needs_override} = Mojo::JSON->true;
826
827     # bibli-level mock doesn't include library_1 as valid pickup location
828     $library_1_api->{needs_override} = Mojo::JSON->true;
829
830     $t->get_ok( "//$userid:$password@/api/v1/holds/"
831           . $hold_1->id
832           . "/pickup_locations?_order_by=marc_org_code" )
833       ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] );
834
835     $schema->storage->txn_rollback;
836 };
837
838 subtest 'edit() tests' => sub {
839
840     plan tests => 14;
841
842     $schema->storage->txn_begin;
843
844     my $password = 'AbcdEFG123';
845
846     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
847     my $patron = $builder->build_object(
848         { class => 'Koha::Patrons', value => { flags => 1 } } );
849     $patron->set_password( { password => $password, skip_validation => 1 } );
850     my $userid = $patron->userid;
851     $builder->build(
852         {
853             source => 'UserPermission',
854             value  => {
855                 borrowernumber => $patron->borrowernumber,
856                 module_bit     => 6,
857                 code           => 'modify_holds_priority',
858             },
859         }
860     );
861
862     # Disable logging
863     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
864     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
865
866     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
867     my $mock_item   = Test::MockModule->new('Koha::Item');
868
869     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
870     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
871     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
872
873     # let's control what Koha::Biblio->pickup_locations returns, for testing
874     $mock_biblio->mock( 'pickup_locations', sub {
875         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
876     });
877     # let's mock what Koha::Item->pickup_locations returns, for testing
878     $mock_item->mock( 'pickup_locations', sub {
879         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
880     });
881
882     my $biblio = $builder->build_sample_biblio;
883     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
884
885     # Test biblio-level holds
886     my $biblio_hold = $builder->build_object(
887         {
888             class => "Koha::Holds",
889             value => {
890                 biblionumber => $biblio->biblionumber,
891                 branchcode   => $library_3->branchcode,
892                 itemnumber   => undef,
893                 priority     => 1,
894             }
895         }
896     );
897
898     my $biblio_hold_data = $biblio_hold->to_api;
899     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
900
901     $t->put_ok( "//$userid:$password@/api/v1/holds/"
902           . $biblio_hold->id
903           => json => $biblio_hold_data )
904       ->status_is(400)
905       ->json_is({ error => 'The supplied pickup location is not valid' });
906
907     $biblio_hold->discard_changes;
908     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
909
910     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
911     $t->put_ok( "//$userid:$password@/api/v1/holds/"
912           . $biblio_hold->id
913           => json => $biblio_hold_data )
914       ->status_is(200);
915
916     $biblio_hold->discard_changes;
917     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
918
919     # Test item-level holds
920     my $item_hold = $builder->build_object(
921         {
922             class => "Koha::Holds",
923             value => {
924                 biblionumber => $biblio->biblionumber,
925                 branchcode   => $library_3->branchcode,
926                 itemnumber   => $item->itemnumber,
927                 priority     => 1,
928             }
929         }
930     );
931
932     my $item_hold_data = $item_hold->to_api;
933     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
934
935     $t->put_ok( "//$userid:$password@/api/v1/holds/"
936           . $item_hold->id
937           => json => $item_hold_data )
938       ->status_is(400)
939       ->json_is({ error => 'The supplied pickup location is not valid' });
940
941     $item_hold->discard_changes;
942     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
943
944     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
945     $t->put_ok( "//$userid:$password@/api/v1/holds/"
946           . $item_hold->id
947           => json => $item_hold_data )
948       ->status_is(200);
949
950     $item_hold->discard_changes;
951     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
952
953     $schema->storage->txn_rollback;
954 };
955
956 subtest 'add() tests' => sub {
957
958     plan tests => 10;
959
960     $schema->storage->txn_begin;
961
962     my $password = 'AbcdEFG123';
963
964     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
965     my $patron = $builder->build_object(
966         { class => 'Koha::Patrons', value => { flags => 1 } } );
967     $patron->set_password( { password => $password, skip_validation => 1 } );
968     my $userid = $patron->userid;
969     $builder->build(
970         {
971             source => 'UserPermission',
972             value  => {
973                 borrowernumber => $patron->borrowernumber,
974                 module_bit     => 6,
975                 code           => 'modify_holds_priority',
976             },
977         }
978     );
979
980     # Disable logging
981     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
982     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
983
984     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
985     my $mock_item   = Test::MockModule->new('Koha::Item');
986
987     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
988     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
989     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
990
991     # let's control what Koha::Biblio->pickup_locations returns, for testing
992     $mock_biblio->mock( 'pickup_locations', sub {
993         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
994     });
995     # let's mock what Koha::Item->pickup_locations returns, for testing
996     $mock_item->mock( 'pickup_locations', sub {
997         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
998     });
999
1000     my $biblio = $builder->build_sample_biblio;
1001     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1002
1003     # Test biblio-level holds
1004     my $biblio_hold = $builder->build_object(
1005         {
1006             class => "Koha::Holds",
1007             value => {
1008                 biblionumber => $biblio->biblionumber,
1009                 branchcode   => $library_3->branchcode,
1010                 itemnumber   => undef,
1011                 priority     => 1,
1012             }
1013         }
1014     );
1015
1016     my $biblio_hold_data = $biblio_hold->to_api;
1017     $biblio_hold->delete;
1018     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
1019     delete $biblio_hold_data->{hold_id};
1020
1021     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
1022       ->status_is(400)
1023       ->json_is({ error => 'The supplied pickup location is not valid' });
1024
1025     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
1026     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
1027       ->status_is(201);
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             }
1039         }
1040     );
1041
1042     my $item_hold_data = $item_hold->to_api;
1043     $item_hold->delete;
1044     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
1045     delete $item_hold->{hold_id};
1046
1047     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1048       ->status_is(400)
1049       ->json_is({ error => 'The supplied pickup location is not valid' });
1050
1051     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1052     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1053       ->status_is(201);
1054
1055     $schema->storage->txn_rollback;
1056 };
1057
1058
1059 subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub {
1060
1061     plan tests => 4;
1062
1063     $schema->storage->txn_begin;
1064
1065     my $password = 'AbcdEFG123';
1066
1067     my $library_1   =  $builder->build_object({ class => 'Koha::Libraries' });
1068     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
1069
1070     my $patron = $builder->build_object(
1071         { class => 'Koha::Patrons', value => { flags => 0 } } );
1072     $patron->set_password( { password => $password, skip_validation => 1 } );
1073     my $userid = $patron->userid;
1074     $builder->build(
1075         {
1076             source => 'UserPermission',
1077             value  => {
1078                 borrowernumber => $patron->borrowernumber,
1079                 module_bit     => 6,
1080                 code           => 'place_holds',
1081             },
1082         }
1083     );
1084
1085     # Disable logging
1086     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
1087     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
1088
1089     my $biblio = $builder->build_sample_biblio;
1090     my $hold = Koha::Holds->find(
1091         AddReserve(
1092             {
1093                 branchcode     => $library_1->branchcode,
1094                 borrowernumber => $patron->borrowernumber,
1095                 biblionumber   => $biblio->biblionumber,
1096                 priority       => 1,
1097             }
1098         )
1099     );
1100
1101     $t->put_ok( "//$userid:$password@/api/v1/holds/"
1102           . $hold->id
1103           . "/pickup_location" => json => $library_2->branchcode )->status_is(200)->json_is($library_2->branchcode);
1104
1105     is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' );
1106
1107     $schema->storage->txn_rollback;
1108 };