Bug 23517: (follow-up) AddReserve expects a priority parameter
[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 under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 use Modern::Perl;
19
20 use Test::More tests => 6;
21 use Test::Mojo;
22 use t::lib::TestBuilder;
23 use t::lib::Mocks;
24
25 use DateTime;
26
27 use C4::Context;
28 use Koha::Patrons;
29 use C4::Reserves;
30 use C4::Items;
31
32 use Koha::Database;
33 use Koha::DateUtils;
34 use Koha::Biblios;
35 use Koha::Biblioitems;
36 use Koha::Items;
37
38 my $schema  = Koha::Database->new->schema;
39 my $builder = t::lib::TestBuilder->new();
40
41 $schema->storage->txn_begin;
42
43 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
44
45 my $t = Test::Mojo->new('Koha::REST::V1');
46
47 my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
48 my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
49 my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype};
50
51 # Generic password for everyone
52 my $password = 'thePassword123';
53
54 # User without any permissions
55 my $nopermission = $builder->build_object({
56     class => 'Koha::Patrons',
57     value => {
58         branchcode   => $branchcode,
59         categorycode => $categorycode,
60         flags        => 0
61     }
62 });
63 $nopermission->set_password( { password => $password, skip_validation => 1 } );
64 my $nopermission_userid = $nopermission->userid;
65
66 my $patron_1 = $builder->build_object(
67     {
68         class => 'Koha::Patrons',
69         value => {
70             categorycode => $categorycode,
71             branchcode   => $branchcode,
72             surname      => 'Test Surname',
73             flags        => 80, #borrowers and reserveforothers flags
74         }
75     }
76 );
77 $patron_1->set_password( { password => $password, skip_validation => 1 } );
78 my $userid_1 = $patron_1->userid;
79
80 my $patron_2 = $builder->build_object(
81     {
82         class => 'Koha::Patrons',
83         value => {
84             categorycode => $categorycode,
85             branchcode   => $branchcode,
86             surname      => 'Test Surname 2',
87             flags        => 16, # borrowers flag
88         }
89     }
90 );
91 $patron_2->set_password( { password => $password, skip_validation => 1 } );
92 my $userid_2 = $patron_2->userid;
93
94 my $patron_3 = $builder->build_object(
95     {
96         class => 'Koha::Patrons',
97         value => {
98             categorycode => $categorycode,
99             branchcode   => $branchcode,
100             surname      => 'Test Surname 3',
101             flags        => 64, # reserveforothers flag
102         }
103     }
104 );
105 $patron_3->set_password( { password => $password, skip_validation => 1 } );
106 my $userid_3 = $patron_3->userid;
107
108 my $biblio_1 = $builder->build_sample_biblio;
109 my $item_1   = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber, itype => $itemtype });
110
111 my $biblio_2 = $builder->build_sample_biblio;
112 my $item_2   = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber, itype => $itemtype });
113
114 my $dbh = C4::Context->dbh;
115 $dbh->do('DELETE FROM reserves');
116 $dbh->do('DELETE FROM issuingrules');
117     $dbh->do(q{
118         INSERT INTO issuingrules (categorycode, branchcode, itemtype, reservesallowed)
119         VALUES (?, ?, ?, ?)
120     }, {}, '*', '*', '*', 1);
121
122 my $reserve_id = C4::Reserves::AddReserve($branchcode, $patron_1->borrowernumber,
123     $biblio_1->biblionumber, undef, 1, undef, undef, undef, '', $item_1->itemnumber);
124
125 # Add another reserve to be able to change first reserve's rank
126 my $reserve_id2 = C4::Reserves::AddReserve($branchcode, $patron_2->borrowernumber,
127     $biblio_1->biblionumber, undef, 2, undef, undef, undef, '', $item_1->itemnumber);
128
129 my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' );
130 my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' );
131
132 my $post_data = {
133     patron_id => int($patron_1->borrowernumber),
134     biblio_id => int($biblio_1->biblionumber),
135     item_id => int($item_1->itemnumber),
136     pickup_library_id => $branchcode,
137     expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }),
138     priority => 2,
139 };
140 my $put_data = {
141     priority => 2,
142     suspended_until => output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }),
143 };
144
145 subtest "Test endpoints without authentication" => sub {
146     plan tests => 8;
147     $t->get_ok('/api/v1/holds')
148       ->status_is(401);
149     $t->post_ok('/api/v1/holds')
150       ->status_is(401);
151     $t->put_ok('/api/v1/holds/0')
152       ->status_is(401);
153     $t->delete_ok('/api/v1/holds/0')
154       ->status_is(401);
155 };
156
157 subtest "Test endpoints without permission" => sub {
158
159     plan tests => 10;
160
161     $t->get_ok( "//$nopermission_userid:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber ) # no permission
162       ->status_is(403);
163
164     $t->get_ok( "//$userid_3:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )    # no permission
165       ->status_is(403);
166
167     $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data )
168       ->status_is(403);
169
170     $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_data )
171       ->status_is(403);
172
173     $t->delete_ok( "//$nopermission_userid:$password@/api/v1/holds/0" )
174       ->status_is(403);
175 };
176
177 subtest "Test endpoints with permission" => sub {
178
179     plan tests => 44;
180
181     $t->get_ok( "//$userid_1:$password@/api/v1/holds" )
182       ->status_is(200)
183       ->json_has('/0')
184       ->json_has('/1')
185       ->json_hasnt('/2');
186
187     $t->get_ok( "//$userid_1:$password@/api/v1/holds?priority=2" )
188       ->status_is(200)
189       ->json_is('/0/patron_id', $patron_2->borrowernumber)
190       ->json_hasnt('/1');
191
192     $t->put_ok( "//$userid_1:$password@/api/v1/holds/$reserve_id" => json => $put_data )
193       ->status_is(200)
194       ->json_is( '/hold_id', $reserve_id )
195       ->json_is( '/suspended_until', output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }) )
196       ->json_is( '/priority', 2 );
197
198     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
199       ->status_is(200);
200
201     $t->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data )
202       ->status_is(404)
203       ->json_has('/error');
204
205     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
206       ->status_is(404)
207       ->json_has('/error');
208
209     $t->get_ok( "//$userid_2:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
210       ->status_is(200)
211       ->json_is([]);
212
213     my $inexisting_borrowernumber = $patron_2->borrowernumber * 2;
214     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=$inexisting_borrowernumber")
215       ->status_is(200)
216       ->json_is([]);
217
218     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" )
219       ->status_is(200);
220
221     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
222       ->status_is(201)
223       ->json_has('/hold_id');
224     # Get id from response
225     $reserve_id = $t->tx->res->json->{hold_id};
226
227     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
228       ->status_is(200)
229       ->json_is('/0/hold_id', $reserve_id)
230       ->json_is('/0/expiration_date', output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }))
231       ->json_is('/0/pickup_library_id', $branchcode);
232
233     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
234       ->status_is(403)
235       ->json_like('/error', qr/itemAlreadyOnHold/);
236
237     $post_data->{biblionumber} = int($biblio_2->biblionumber);
238     $post_data->{itemnumber}   = int($item_2->itemnumber);
239
240     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
241       ->status_is(403)
242       ->json_like('/error', qr/itemAlreadyOnHold/);
243 };
244
245 subtest 'Reserves with itemtype' => sub {
246     plan tests => 9;
247
248     my $post_data = {
249         patron_id => int($patron_1->borrowernumber),
250         biblio_id => int($biblio_1->biblionumber),
251         pickup_library_id => $branchcode,
252         item_type => $itemtype,
253     };
254
255     $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" )
256       ->status_is(200);
257
258     $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data )
259       ->status_is(201)
260       ->json_has('/hold_id');
261
262     $reserve_id = $t->tx->res->json->{hold_id};
263
264     $t->get_ok( "//$userid_1:$password@/api/v1/holds?patron_id=" . $patron_1->borrowernumber )
265       ->status_is(200)
266       ->json_is('/0/hold_id', $reserve_id)
267       ->json_is('/0/item_type', $itemtype);
268 };
269
270 $schema->storage->txn_rollback;
271
272 subtest 'suspend and resume tests' => sub {
273
274     plan tests => 21;
275
276     $schema->storage->txn_begin;
277
278     my $password = 'AbcdEFG123';
279
280     my $patron = $builder->build_object(
281         { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } );
282     $patron->set_password({ password => $password, skip_validation => 1 });
283     my $userid = $patron->userid;
284
285     # Disable logging
286     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
287     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
288
289     my $hold = $builder->build_object(
290         {   class => 'Koha::Holds',
291             value => { suspend => 0, suspend_until => undef, waitingdate => undef }
292         }
293     );
294
295     ok( !$hold->is_suspended, 'Hold is not suspended' );
296     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
297         ->status_is( 201, 'Hold suspension created' );
298
299     $hold->discard_changes;    # refresh object
300
301     ok( $hold->is_suspended, 'Hold is suspended' );
302     $t->json_is(
303         '/end_date',
304         output_pref(
305             {   dt         => dt_from_string( $hold->suspend_until ),
306                 dateformat => 'rfc3339',
307                 dateonly   => 1
308             }
309         )
310     );
311
312     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
313       ->status_is( 204, "Correct status when deleting a resource" )
314       ->json_is( undef );
315
316     # Pass a an expiration date for the suspension
317     my $date = dt_from_string()->add( days => 5 );
318     $t->post_ok(
319               "//$userid:$password@/api/v1/holds/"
320             . $hold->id
321             . "/suspension" => json => {
322             end_date =>
323                 output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } )
324             }
325     )->status_is( 201, 'Hold suspension created' )
326         ->json_is( '/end_date',
327         output_pref( { dt => $date, dateformat => 'rfc3339', dateonly => 1 } ) )
328         ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' );
329
330     $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
331       ->status_is( 204, "Correct status when deleting a resource" )
332       ->json_is( undef );
333
334     $hold->set_waiting->discard_changes;
335
336     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
337       ->status_is( 400, 'Cannot suspend waiting hold' )
338       ->json_is( '/error', 'Found hold cannot be suspended. Status=W' );
339
340     $hold->set_waiting(1)->discard_changes;
341
342     $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" )
343       ->status_is( 400, 'Cannot suspend waiting hold' )
344       ->json_is( '/error', 'Found hold cannot be suspended. Status=T' );
345
346     $schema->storage->txn_rollback;
347 };
348
349 subtest 'PUT /holds/{hold_id}/priority tests' => sub {
350
351     plan tests => 14;
352
353     $schema->storage->txn_begin;
354
355     my $password = 'AbcdEFG123';
356
357     my $library  = $builder->build_object({ class => 'Koha::Libraries' });
358     my $patron_np = $builder->build_object(
359         { class => 'Koha::Patrons', value => { flags => 0 } } );
360     $patron_np->set_password( { password => $password, skip_validation => 1 } );
361     my $userid_np = $patron_np->userid;
362
363     my $patron = $builder->build_object(
364         { class => 'Koha::Patrons', value => { flags => 0 } } );
365     $patron->set_password( { password => $password, skip_validation => 1 } );
366     my $userid = $patron->userid;
367     $builder->build(
368         {
369             source => 'UserPermission',
370             value  => {
371                 borrowernumber => $patron->borrowernumber,
372                 module_bit     => 6,
373                 code           => 'modify_holds_priority',
374             },
375         }
376     );
377
378     # Disable logging
379     t::lib::Mocks::mock_preference( 'HoldsLog',      0 );
380     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
381
382     my $biblio   = $builder->build_sample_biblio;
383     my $patron_1 = $builder->build_object(
384         {
385             class => 'Koha::Patrons',
386             value => { branchcode => $library->branchcode }
387         }
388     );
389     my $patron_2 = $builder->build_object(
390         {
391             class => 'Koha::Patrons',
392             value => { branchcode => $library->branchcode }
393         }
394     );
395     my $patron_3 = $builder->build_object(
396         {
397             class => 'Koha::Patrons',
398             value => { branchcode => $library->branchcode }
399         }
400     );
401
402     my $hold_1 = Koha::Holds->find(
403         AddReserve(
404             $library->branchcode,  $patron_1->borrowernumber,
405             $biblio->biblionumber, undef,
406             1
407         )
408     );
409     my $hold_2 = Koha::Holds->find(
410         AddReserve(
411             $library->branchcode,  $patron_2->borrowernumber,
412             $biblio->biblionumber, undef,
413             2
414         )
415     );
416     my $hold_3 = Koha::Holds->find(
417         AddReserve(
418             $library->branchcode,  $patron_3->borrowernumber,
419             $biblio->biblionumber, undef,
420             3
421         )
422     );
423
424     $t->put_ok( "//$userid_np:$password@/api/v1/holds/"
425           . $hold_3->id
426           . "/priority" => json => 1 )->status_is(403);
427
428     $t->put_ok( "//$userid:$password@/api/v1/holds/"
429           . $hold_3->id
430           . "/priority" => json => 1 )->status_is(200)->json_is(1);
431
432     is( $hold_1->discard_changes->priority, 2, 'Priority adjusted correctly' );
433     is( $hold_2->discard_changes->priority, 3, 'Priority adjusted correctly' );
434     is( $hold_3->discard_changes->priority, 1, 'Priority adjusted correctly' );
435
436     $t->put_ok( "//$userid:$password@/api/v1/holds/"
437           . $hold_3->id
438           . "/priority" => json => 3 )->status_is(200)->json_is(3);
439
440     is( $hold_1->discard_changes->priority, 1, 'Priority adjusted correctly' );
441     is( $hold_2->discard_changes->priority, 2, 'Priority adjusted correctly' );
442     is( $hold_3->discard_changes->priority, 3, 'Priority adjusted correctly' );
443
444     $schema->storage->txn_rollback;
445 };