Bug 27015: 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 => 12;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::TestBuilder;
24 use t::lib::Mocks;
25
26 use DateTime;
27
28 use C4::Context;
29 use Koha::Patrons;
30 use C4::Reserves;
31 use C4::Items;
32
33 use Koha::Database;
34 use Koha::DateUtils;
35 use Koha::Biblios;
36 use Koha::Biblioitems;
37 use Koha::Items;
38 use Koha::CirculationRules;
39
40 my $schema  = Koha::Database->new->schema;
41 my $builder = t::lib::TestBuilder->new();
42
43 $schema->storage->txn_begin;
44
45 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
46
47 my $t = Test::Mojo->new('Koha::REST::V1');
48
49 my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
50 my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
51 my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode};
52 my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype};
53
54 # Generic password for everyone
55 my $password = 'thePassword123';
56
57 # User without any permissions
58 my $nopermission = $builder->build_object({
59     class => 'Koha::Patrons',
60     value => {
61         branchcode   => $branchcode,
62         categorycode => $categorycode,
63         flags        => 0
64     }
65 });
66 $nopermission->set_password( { password => $password, skip_validation => 1 } );
67 my $nopermission_userid = $nopermission->userid;
68
69 my $patron_1 = $builder->build_object(
70     {
71         class => 'Koha::Patrons',
72         value => {
73             categorycode => $categorycode,
74             branchcode   => $branchcode,
75             surname      => 'Test Surname',
76             flags        => 80, #borrowers and reserveforothers flags
77         }
78     }
79 );
80 $patron_1->set_password( { password => $password, skip_validation => 1 } );
81 my $userid_1 = $patron_1->userid;
82
83 my $patron_2 = $builder->build_object(
84     {
85         class => 'Koha::Patrons',
86         value => {
87             categorycode => $categorycode,
88             branchcode   => $branchcode,
89             surname      => 'Test Surname 2',
90             flags        => 16, # borrowers flag
91         }
92     }
93 );
94 $patron_2->set_password( { password => $password, skip_validation => 1 } );
95 my $userid_2 = $patron_2->userid;
96
97 my $patron_3 = $builder->build_object(
98     {
99         class => 'Koha::Patrons',
100         value => {
101             categorycode => $categorycode,
102             branchcode   => $branchcode,
103             surname      => 'Test Surname 3',
104             flags        => 64, # reserveforothers flag
105         }
106     }
107 );
108 $patron_3->set_password( { password => $password, skip_validation => 1 } );
109 my $userid_3 = $patron_3->userid;
110
111 my $biblio_1 = $builder->build_sample_biblio;
112 my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber, itype => $itemtype });
113
114 my $biblio_2 = $builder->build_sample_biblio;
115 my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber, itype => $itemtype });
116
117 my $dbh = C4::Context->dbh;
118 $dbh->do('DELETE FROM reserves');
119 Koha::CirculationRules->search()->delete();
120 Koha::CirculationRules->set_rules(
121     {
122         categorycode => undef,
123         branchcode   => undef,
124         itemtype     => undef,
125         rules        => {
126             reservesallowed => 1,
127             holds_per_record => 99
128         }
129     }
130 );
131
132 my $reserve_id = C4::Reserves::AddReserve(
133     {
134         branchcode     => $branchcode,
135         borrowernumber => $patron_1->borrowernumber,
136         biblionumber   => $biblio_1->biblionumber,
137         priority       => 1,
138         itemnumber     => $item_1->itemnumber,
139     }
140 );
141
142 # Add another reserve to be able to change first reserve's rank
143 my $reserve_id2 = C4::Reserves::AddReserve(
144     {
145         branchcode     => $branchcode,
146         borrowernumber => $patron_2->borrowernumber,
147         biblionumber   => $biblio_1->biblionumber,
148         priority       => 2,
149         itemnumber     => $item_1->itemnumber,
150     }
151 );
152
153 my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' );
154 my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
155
156 my $post_data = {
157     patron_id => int($patron_1->borrowernumber),
158     biblio_id => int($biblio_1->biblionumber),
159     item_id => int($item_1->itemnumber),
160     pickup_library_id => $branchcode,
161     expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
162     priority => 2,
163 };
164 my $put_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->put_ok('/api/v1/holds/0')
176       ->status_is(401);
177     $t->delete_ok('/api/v1/holds/0')
178       ->status_is(401);
179 };
180
181 subtest "Test endpoints without permission" => sub {
182
183     plan tests => 10;
184
185     $t->get_ok( "//$nopermission_userid:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber ) # no permission
186       ->status_is(403);
187
188     $t->get_ok( "//$userid_3:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )    # no permission
189       ->status_is(403);
190
191     $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data )
192       ->status_is(403);
193
194     $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_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->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data )
221       ->status_is(404)
222       ->json_has('/error');
223
224     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
225       ->status_is(404)
226       ->json_has('/error');
227
228     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
229       ->status_is(200)
230       ->json_is([]);
231
232     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
233     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
234       ->status_is(200)
235       ->json_is([]);
236
237     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
238       ->status_is(204, 'SWAGGER3.2.4')
239       ->content_is('', 'SWAGGER3.3.4');
240
241     # Make sure pickup location checks doesn't get in the middle
242     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
243     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
244     my $mock_item   = Test::MockModule->new('Koha::Item');
245     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
246
247     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
248       ->status_is(201)
249       ->json_has('/hold_id');
250
251     # Get id from response
252     $reserve_id = $t->tx->res->json->{hold_id};
253
254     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
255       ->status_is(200)
256       ->json_is('/0/hold_id', $reserve_id)
257       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
258       ->json_is('/0/pickup_library_id', $branchcode);
259
260     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
261       ->status_is(403)
262       ->json_like('/error', qr/itemAlreadyOnHold/);
263
264     $post_data->{biblionumber} = int($biblio_2->biblionumber);
265     $post_data->{itemnumber}   = int($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/itemAlreadyOnHold/);
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 => int($patron_1->borrowernumber),
329         biblio_id => int($biblio_1->biblionumber),
330         item_id => int($item_1->itemnumber),
331         pickup_library_id => $branchcode,
332         expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
333         hold_date => output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }),
334         priority => 2,
335     };
336
337     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 );
338
339     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
340       ->status_is(400)
341       ->json_has('/error');
342
343     t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 );
344
345     # Make sure pickup location checks doesn't get in the middle
346     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
347     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
348     my $mock_item   = Test::MockModule->new('Koha::Item');
349     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
350
351     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
352       ->status_is(201)
353       ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }));
354 };
355
356 subtest 'test AllowHoldPolicyOverride' => sub {
357
358     plan tests => 5;
359
360     $dbh->do('DELETE FROM reserves');
361
362     Koha::CirculationRules->set_rules(
363         {
364             itemtype     => undef,
365             branchcode   => undef,
366             rules        => {
367                 holdallowed => 1
368             }
369         }
370     );
371
372     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
373
374     # Make sure pickup location checks doesn't get in the middle
375     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
376     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
377     my $mock_item   = Test::MockModule->new('Koha::Item');
378     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
379
380     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
381       ->status_is(403)
382       ->json_has('/error');
383
384     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 );
385
386     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
387       ->status_is(201);
388 };
389
390 $schema->storage->txn_rollback;
391
392 subtest 'suspend and resume tests' => sub {
393
394     plan tests => 24;
395
396     $schema->storage->txn_begin;
397
398     my $password = 'AbcdEFG123';
399
400     my $patron = $builder->build_object(
401         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
402     $patron->set_password({ password => $password, skip_validation => 1 });
403     my $userid = $patron->userid;
404
405     # Disable logging
406     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
407     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
408
409     my $hold = $builder->build_object(
410         {   class => 'Koha::Holds',
411             value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef }
412         }
413     );
414
415     ok( !$hold->is_suspended, 'Hold is not suspended' );
416     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
417         ->status_is( 201, 'Hold suspension created' );
418
419     $hold->discard_changes;    # refresh object
420
421     ok( $hold->is_suspended, 'Hold is suspended' );
422     $t->json_is('/end_date', undef, 'Hold suspension has no end date');
423
424     my $end_date = output_pref({
425       dt         => dt_from_string( undef ),
426       dateformat => 'rfc3339',
427       dateonly   => 1
428     });
429
430     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } );
431
432     $hold->discard_changes;    # refresh object
433
434     ok( $hold->is_suspended, 'Hold is suspended' );
435     $t->json_is(
436       '/end_date',
437       output_pref({
438         dt         => dt_from_string( $hold->suspend_until ),
439         dateformat => 'rfc3339',
440         dateonly   => 1
441       }),
442       'Hold suspension has correct end date'
443     );
444
445     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
446       ->status_is(204, 'SWAGGER3.2.4')
447       ->content_is('', 'SWAGGER3.3.4');
448
449     # Pass a an expiration date for the suspension
450     my $date = dt_from_string()->add( days => 5 );
451     $t->post_ok(
452               "//$userid:$password@/api/v1/holds/"
453             . $hold->id
454             . "/suspension" => json => {
455             end_date =>
456                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
457             }
458     )->status_is( 201, 'Hold suspension created' )
459         ->json_is( '/end_date',
460         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
461         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
462
463     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
464       ->status_is(204, 'SWAGGER3.2.4')
465       ->content_is('', 'SWAGGER3.3.4');
466
467     $hold->set_waiting->discard_changes;
468
469     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
470       ->status_is( 400, 'Cannot suspend waiting hold' )
471       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
472
473     $hold->set_transfer->discard_changes;
474
475     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
476       ->status_is( 400, 'Cannot suspend hold on transfer' )
477       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
478
479     $schema->storage->txn_rollback;
480 };
481
482 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
483
484     plan tests => 14;
485
486     $schema->storage->txn_begin;
487
488     my $password = 'AbcdEFG123';
489
490     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
491     my $patron_np = $builder->build_object(
492         { class => 'Koha::Patrons', value => { flags => 0 } } );
493     $patron_np->set_password( { password => $password, skip_validation => 1 } );
494     my $userid_np = $patron_np->userid;
495
496     my $patron = $builder->build_object(
497         { class => 'Koha::Patrons', value => { flags => 0 } } );
498     $patron->set_password( { password => $password, skip_validation => 1 } );
499     my $userid = $patron->userid;
500     $builder->build(
501         {
502             source => 'UserPermission',
503             value  => {
504                 borrowernumber => $patron->borrowernumber,
505                 module_bit     => 6,
506                 code           => 'modify_holds_priority',
507             },
508         }
509     );
510
511     # Disable logging
512     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
513     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
514
515     my $biblio   = $builder->build_sample_biblio;
516     my $patron_1 = $builder->build_object(
517         {
518             class => 'Koha::Patrons',
519             value => { branchcode => $library->branchcode }
520         }
521     );
522     my $patron_2 = $builder->build_object(
523         {
524             class => 'Koha::Patrons',
525             value => { branchcode => $library->branchcode }
526         }
527     );
528     my $patron_3 = $builder->build_object(
529         {
530             class => 'Koha::Patrons',
531             value => { branchcode => $library->branchcode }
532         }
533     );
534
535     my $hold_1 = Koha::Holds->find(
536         AddReserve(
537             {
538                 branchcode     => $library->branchcode,
539                 borrowernumber => $patron_1->borrowernumber,
540                 biblionumber   => $biblio->biblionumber,
541                 priority       => 1,
542             }
543         )
544     );
545     my $hold_2 = Koha::Holds->find(
546         AddReserve(
547             {
548                 branchcode     => $library->branchcode,
549                 borrowernumber => $patron_2->borrowernumber,
550                 biblionumber   => $biblio->biblionumber,
551                 priority       => 2,
552             }
553         )
554     );
555     my $hold_3 = Koha::Holds->find(
556         AddReserve(
557             {
558                 branchcode     => $library->branchcode,
559                 borrowernumber => $patron_3->borrowernumber,
560                 biblionumber   => $biblio->biblionumber,
561                 priority       => 3,
562             }
563         )
564     );
565
566     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
567           . $hold_3->id
568           . "/priority" => json => 1 )->status_is(403);
569
570     $t->put_ok( "//$userid:$password@/api/v1/holds/"
571           . $hold_3->id
572           . "/priority" => json => 1 )->status_is(200)->json_is(1);
573
574     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
575     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
576     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
577
578     $t->put_ok( "//$userid:$password@/api/v1/holds/"
579           . $hold_3->id
580           . "/priority" => json => 3 )->status_is(200)->json_is(3);
581
582     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
583     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
584     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
585
586     $schema->storage->txn_rollback;
587 };
588
589 subtest 'add() tests (maxreserves behaviour)' => sub {
590
591     plan tests => 7;
592
593     $schema->storage->txn_begin;
594
595     $dbh->do('DELETE FROM reserves');
596
597     Koha::CirculationRules->new->delete;
598
599     my $password = 'AbcdEFG123';
600
601     my $patron = $builder->build_object(
602         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
603     $patron->set_password({ password => $password, skip_validation => 1 });
604     my $userid = $patron->userid;
605
606     Koha::CirculationRules->set_rules(
607         {
608             itemtype     => undef,
609             branchcode   => undef,
610             categorycode => undef,
611             rules        => {
612                 reservesallowed => 3
613             }
614         }
615     );
616
617     Koha::CirculationRules->set_rules(
618         {
619             branchcode   => undef,
620             categorycode => $patron->categorycode,
621             rules        => {
622                 max_holds   => 4,
623             }
624         }
625     );
626
627     my $biblio_1 = $builder->build_sample_biblio;
628     my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber });
629     my $biblio_2 = $builder->build_sample_biblio;
630     my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber });
631     my $biblio_3 = $builder->build_sample_biblio;
632     my $item_3   = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber });
633
634     # Make sure pickup location checks doesn't get in the middle
635     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
636     $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; });
637     my $mock_item   = Test::MockModule->new('Koha::Item');
638     $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search });
639
640     # Disable logging
641     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
642     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
643     t::lib::Mocks::mock_preference( 'maxreserves',   2 );
644     t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 );
645
646     my $post_data = {
647         patron_id => $patron->borrowernumber,
648         biblio_id => $biblio_1->biblionumber,
649         pickup_library_id => $item_1->home_branch->branchcode,
650         item_type => $item_1->itype,
651     };
652
653     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
654       ->status_is(201);
655
656     $post_data = {
657         patron_id => $patron->borrowernumber,
658         biblio_id => $biblio_2->biblionumber,
659         pickup_library_id => $item_2->home_branch->branchcode,
660         item_id   => $item_2->itemnumber
661     };
662
663     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
664       ->status_is(201);
665
666     $post_data = {
667         patron_id => $patron->borrowernumber,
668         biblio_id => $biblio_3->biblionumber,
669         pickup_library_id => $item_1->home_branch->branchcode,
670         item_id   => $item_3->itemnumber
671     };
672
673     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )
674       ->status_is(403)
675       ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } );
676
677     $schema->storage->txn_rollback;
678 };
679
680 subtest 'pickup_locations() tests' => sub {
681
682     plan tests => 9;
683
684     $schema->storage->txn_begin;
685
686     my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A' } });
687     my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B' } });
688     my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C' } });
689
690     my $patron = $builder->build_object(
691         {
692             class => 'Koha::Patrons',
693             value => { userid => 'tomasito', flags => 0 }
694         }
695     );
696     $patron->set_password( { password => $password, skip_validation => 1 } );
697     my $userid = $patron->userid;
698     $builder->build(
699         {
700             source => 'UserPermission',
701             value  => {
702                 borrowernumber => $patron->borrowernumber,
703                 module_bit     => 6,
704                 code           => 'place_holds',
705             },
706         }
707     );
708
709     my $item_class = Test::MockModule->new('Koha::Item');
710     $item_class->mock(
711         'pickup_locations',
712         sub {
713             my ( $self, $params ) = @_;
714             my $mock_patron = $params->{patron};
715             is( $mock_patron->borrowernumber,
716                 $patron->borrowernumber, 'Patron passed correctly' );
717             return Koha::Libraries->search(
718                 {
719                     branchcode => {
720                         '-in' => [
721                             $library_1->branchcode,
722                             $library_2->branchcode
723                         ]
724                     }
725                 },
726                 {   # we make sure no surprises in the order of the result
727                     order_by => { '-asc' => 'marcorgcode' }
728                 }
729             );
730         }
731     );
732
733     my $biblio_class = Test::MockModule->new('Koha::Biblio');
734     $biblio_class->mock(
735         'pickup_locations',
736         sub {
737             my ( $self, $params ) = @_;
738             my $mock_patron = $params->{patron};
739             is( $mock_patron->borrowernumber,
740                 $patron->borrowernumber, 'Patron passed correctly' );
741             return Koha::Libraries->search(
742                 {
743                     branchcode => {
744                         '-in' => [
745                             $library_2->branchcode,
746                             $library_3->branchcode
747                         ]
748                     }
749                 },
750                 {   # we make sure no surprises in the order of the result
751                     order_by => { '-asc' => 'marcorgcode' }
752                 }
753             );
754         }
755     );
756
757     my $item = $builder->build_sample_item;
758
759     # biblio-level hold
760     my $hold_1 = $builder->build_object(
761         {
762             class => 'Koha::Holds',
763             value => {
764                 itemnumber     => undef,
765                 biblionumber   => $item->biblionumber,
766                 borrowernumber => $patron->borrowernumber
767             }
768         }
769     );
770     # item-level hold
771     my $hold_2 = $builder->build_object(
772         {
773             class => 'Koha::Holds',
774             value => {
775                 itemnumber     => $item->itemnumber,
776                 biblionumber   => $item->biblionumber,
777                 borrowernumber => $patron->borrowernumber
778             }
779         }
780     );
781
782     $t->get_ok( "//$userid:$password@/api/v1/holds/"
783           . $hold_1->id
784           . "/pickup_locations" )
785       ->json_is( [ $library_2->to_api, $library_3->to_api ] );
786
787     $t->get_ok( "//$userid:$password@/api/v1/holds/"
788           . $hold_2->id
789           . "/pickup_locations" )
790       ->json_is( [ $library_1->to_api, $library_2->to_api ] );
791
792     # filtering works!
793     $t->get_ok( "//$userid:$password@/api/v1/holds/"
794           . $hold_2->id
795           . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' )
796       ->json_is( [ $library_1->to_api ] );
797
798
799     $schema->storage->txn_rollback;
800 };
801
802 subtest 'edit() tests' => sub {
803
804     plan tests => 14;
805
806     $schema->storage->txn_begin;
807
808     my $password = 'AbcdEFG123';
809
810     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
811     my $patron = $builder->build_object(
812         { class => 'Koha::Patrons', value => { flags => 1 } } );
813     $patron->set_password( { password => $password, skip_validation => 1 } );
814     my $userid = $patron->userid;
815     $builder->build(
816         {
817             source => 'UserPermission',
818             value  => {
819                 borrowernumber => $patron->borrowernumber,
820                 module_bit     => 6,
821                 code           => 'modify_holds_priority',
822             },
823         }
824     );
825
826     # Disable logging
827     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
828     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
829
830     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
831     my $mock_item   = Test::MockModule->new('Koha::Item');
832
833     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
834     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
835     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
836
837     # let's control what Koha::Biblio->pickup_locations returns, for testing
838     $mock_biblio->mock( 'pickup_locations', sub {
839         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
840     });
841     # let's mock what Koha::Item->pickup_locations returns, for testing
842     $mock_item->mock( 'pickup_locations', sub {
843         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
844     });
845
846     my $biblio = $builder->build_sample_biblio;
847     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
848
849     # Test biblio-level holds
850     my $biblio_hold = $builder->build_object(
851         {
852             class => "Koha::Holds",
853             value => {
854                 biblionumber => $biblio->biblionumber,
855                 branchcode   => $library_3->branchcode,
856                 itemnumber   => undef,
857                 priority     => 1,
858             }
859         }
860     );
861
862     my $biblio_hold_data = $biblio_hold->to_api;
863     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
864
865     $t->put_ok( "//$userid:$password@/api/v1/holds/"
866           . $biblio_hold->id
867           => json => $biblio_hold_data )
868       ->status_is(400)
869       ->json_is({ error => 'The supplied pickup location is not valid' });
870
871     $biblio_hold->discard_changes;
872     is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
873
874     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
875     $t->put_ok( "//$userid:$password@/api/v1/holds/"
876           . $biblio_hold->id
877           => json => $biblio_hold_data )
878       ->status_is(200);
879
880     $biblio_hold->discard_changes;
881     is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
882
883     # Test item-level holds
884     my $item_hold = $builder->build_object(
885         {
886             class => "Koha::Holds",
887             value => {
888                 biblionumber => $biblio->biblionumber,
889                 branchcode   => $library_3->branchcode,
890                 itemnumber   => $item->itemnumber,
891                 priority     => 1,
892             }
893         }
894     );
895
896     my $item_hold_data = $item_hold->to_api;
897     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
898
899     $t->put_ok( "//$userid:$password@/api/v1/holds/"
900           . $item_hold->id
901           => json => $item_hold_data )
902       ->status_is(400)
903       ->json_is({ error => 'The supplied pickup location is not valid' });
904
905     $item_hold->discard_changes;
906     is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' );
907
908     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
909     $t->put_ok( "//$userid:$password@/api/v1/holds/"
910           . $item_hold->id
911           => json => $item_hold_data )
912       ->status_is(200);
913
914     $item_hold->discard_changes;
915     is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' );
916
917     $schema->storage->txn_rollback;
918 };
919
920 subtest 'add() tests' => sub {
921
922     plan tests => 10;
923
924     $schema->storage->txn_begin;
925
926     my $password = 'AbcdEFG123';
927
928     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
929     my $patron = $builder->build_object(
930         { class => 'Koha::Patrons', value => { flags => 1 } } );
931     $patron->set_password( { password => $password, skip_validation => 1 } );
932     my $userid = $patron->userid;
933     $builder->build(
934         {
935             source => 'UserPermission',
936             value  => {
937                 borrowernumber => $patron->borrowernumber,
938                 module_bit     => 6,
939                 code           => 'modify_holds_priority',
940             },
941         }
942     );
943
944     # Disable logging
945     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
946     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
947
948     my $mock_biblio = Test::MockModule->new('Koha::Biblio');
949     my $mock_item   = Test::MockModule->new('Koha::Item');
950
951     my $library_1 = $builder->build_object({ class => 'Koha::Libraries' });
952     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
953     my $library_3 = $builder->build_object({ class => 'Koha::Libraries' });
954
955     # let's control what Koha::Biblio->pickup_locations returns, for testing
956     $mock_biblio->mock( 'pickup_locations', sub {
957         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
958     });
959     # let's mock what Koha::Item->pickup_locations returns, for testing
960     $mock_item->mock( 'pickup_locations', sub {
961         return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } );
962     });
963
964     my $biblio = $builder->build_sample_biblio;
965     my $item   = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
966
967     # Test biblio-level holds
968     my $biblio_hold = $builder->build_object(
969         {
970             class => "Koha::Holds",
971             value => {
972                 biblionumber => $biblio->biblionumber,
973                 branchcode   => $library_3->branchcode,
974                 itemnumber   => undef,
975                 priority     => 1,
976             }
977         }
978     );
979
980     my $biblio_hold_data = $biblio_hold->to_api;
981     $biblio_hold->delete;
982     $biblio_hold_data->{pickup_library_id} = $library_1->branchcode;
983     delete $biblio_hold_data->{hold_id};
984
985     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data )
986       ->status_is(400)
987       ->json_is({ error => 'The supplied pickup location is not valid' });
988
989     $biblio_hold_data->{pickup_library_id} = $library_2->branchcode;
990     $t->post_ok( "//$userid:$password@/api/v1/holds"  => json => $biblio_hold_data )
991       ->status_is(201);
992
993     # Test item-level holds
994     my $item_hold = $builder->build_object(
995         {
996             class => "Koha::Holds",
997             value => {
998                 biblionumber => $biblio->biblionumber,
999                 branchcode   => $library_3->branchcode,
1000                 itemnumber   => $item->itemnumber,
1001                 priority     => 1,
1002             }
1003         }
1004     );
1005
1006     my $item_hold_data = $item_hold->to_api;
1007     $item_hold->delete;
1008     $item_hold_data->{pickup_library_id} = $library_1->branchcode;
1009     delete $item_hold->{hold_id};
1010
1011     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1012       ->status_is(400)
1013       ->json_is({ error => 'The supplied pickup location is not valid' });
1014
1015     $item_hold_data->{pickup_library_id} = $library_2->branchcode;
1016     $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data )
1017       ->status_is(201);
1018
1019     $schema->storage->txn_rollback;
1020 };