Bug 23336: (QA follow-up) Minor fixes
[koha.git] / t / db_dependent / api / v1 / checkouts.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 => 101;
21 use Test::MockModule;
22 use Test::Mojo;
23 use t::lib::Mocks;
24 use t::lib::TestBuilder;
25
26 use DateTime;
27
28 use C4::Context;
29 use C4::Circulation qw( AddIssue AddReturn CanBookBeIssued );
30
31 use Koha::Database;
32 use Koha::DateUtils qw( dt_from_string output_pref );
33 use Koha::Token;
34
35 my $schema = Koha::Database->schema;
36 my $builder = t::lib::TestBuilder->new;
37
38 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
39 my $t = Test::Mojo->new('Koha::REST::V1');
40
41 $schema->storage->txn_begin;
42
43 my $dbh = C4::Context->dbh;
44
45 my $librarian = $builder->build_object({
46     class => 'Koha::Patrons',
47     value => { flags => 2 }
48 });
49 my $password = 'thePassword123';
50 $librarian->set_password({ password => $password, skip_validation => 1 });
51 my $userid = $librarian->userid;
52
53 my $patron = $builder->build_object({
54     class => 'Koha::Patrons',
55     value => { flags => 0 }
56 });
57 my $unauth_password = 'thePassword000';
58 $patron->set_password({ password => $unauth_password, skip_validattion => 1 });
59 my $unauth_userid = $patron->userid;
60 my $patron_id = $patron->borrowernumber;
61
62 my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode };
63
64 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
65   ->status_is(200)
66   ->json_is([]);
67
68 my $notexisting_patron_id = $patron_id + 1;
69 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$notexisting_patron_id" )
70   ->status_is(200)
71   ->json_is([]);
72
73 Koha::CirculationRules->set_rules(
74     {
75         categorycode => undef,
76         itemtype     => undef,
77         branchcode   => undef,
78         rules        => {
79             renewalperiod => 7,
80             renewalsallowed => 1,
81             issuelength => 5,
82         }
83     }
84 );
85
86 my $item1 = $builder->build_sample_item;
87 my $item2 = $builder->build_sample_item;
88 my $item3 = $builder->build_sample_item;
89 my $item4 = $builder->build_sample_item;
90
91 my $date_due = DateTime->now->add(weeks => 2);
92 my $issue1 = C4::Circulation::AddIssue($patron->unblessed, $item1->barcode, $date_due);
93 my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due );
94 my $issue2 = C4::Circulation::AddIssue($patron->unblessed, $item2->barcode, $date_due);
95 my $date_due2 = Koha::DateUtils::dt_from_string( $issue2->date_due );
96 my $issue3 = C4::Circulation::AddIssue($librarian->unblessed, $item3->barcode, $date_due);
97 my $date_due3 = Koha::DateUtils::dt_from_string( $issue3->date_due );
98 my $issue4 = C4::Circulation::AddIssue($patron->unblessed, $item4->barcode);
99 C4::Circulation::AddReturn($item4->barcode, $branchcode);
100
101 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id" )
102   ->status_is(200)
103   ->json_is('/0/patron_id' => $patron_id)
104   ->json_is('/0/item_id' => $item1->itemnumber)
105   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
106   ->json_is('/1/patron_id' => $patron_id)
107   ->json_is('/1/item_id' => $item2->itemnumber)
108   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
109   ->json_hasnt('/2');
110
111 # Test checked_in parameter, zero means, the response is same as without it
112 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=0" )
113   ->status_is(200)
114   ->json_is('/0/patron_id' => $patron_id)
115   ->json_is('/0/item_id' => $item1->itemnumber)
116   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
117   ->json_is('/1/patron_id' => $patron_id)
118   ->json_is('/1/item_id' => $item2->itemnumber)
119   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
120   ->json_hasnt('/2');
121
122 # Test checked_in parameter, one measn, the checked in checkout is in the response too
123 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&checked_in=1" )
124   ->status_is(200)
125   ->json_is('/0/patron_id' => $patron_id)
126   ->json_is('/0/item_id' => $item4->itemnumber)
127   ->json_hasnt('/1');
128
129 $t->get_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id )
130   ->status_is(403)
131   ->json_is({ error => "Authorization failure. Missing required permission(s).",
132               required_permissions => { circulate => "circulate_remaining_permissions" }
133             });
134
135 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id")
136   ->status_is(200)
137   ->json_is('/0/patron_id' => $patron_id)
138   ->json_is('/0/item_id' => $item1->itemnumber)
139   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
140   ->json_is('/1/patron_id' => $patron_id)
141   ->json_is('/1/item_id' => $item2->itemnumber)
142   ->json_is('/1/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
143   ->json_hasnt('/2');
144
145 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=1")
146   ->status_is(200)
147   ->header_is('X-Total-Count', '2')
148   ->header_like('Link', qr|rel="next"|)
149   ->header_like('Link', qr|rel="first"|)
150   ->header_like('Link', qr|rel="last"|)
151   ->json_is('/0/patron_id' => $patron_id)
152   ->json_is('/0/item_id' => $item1->itemnumber)
153   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
154   ->json_hasnt('/1');
155
156 $t->get_ok( "//$userid:$password@/api/v1/checkouts?patron_id=$patron_id&_per_page=1&_page=2")
157   ->status_is(200)
158   ->header_is('X-Total-Count', '2')
159   ->header_like('Link', qr|rel="prev"|)
160   ->header_like('Link', qr|rel="first"|)
161   ->header_like('Link', qr|rel="last"|)
162   ->json_is('/0/patron_id' => $patron_id)
163   ->json_is('/0/item_id' => $item2->itemnumber)
164   ->json_is('/0/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due2 }) )
165   ->json_hasnt('/1');
166
167 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
168   ->status_is(200)
169   ->json_is('/patron_id' => $patron_id)
170   ->json_is('/item_id' => $item1->itemnumber)
171   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) )
172   ->json_hasnt('/1');
173
174 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id)
175   ->status_is(200)
176   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $date_due1 }) );
177
178 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id)
179   ->status_is(200)
180   ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $date_due2 }) );
181
182 my $expected_datedue = $date_due
183     ->set_time_zone('local')
184     ->add(days => 7)
185     ->set(hour => 23, minute => 59, second => 0);
186
187 $t->post_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
188   ->status_is(201)
189   ->json_is('/due_date' => output_pref( { dateformat => "rfc3339", dt => $expected_datedue }) )
190   ->header_is(Location => "/api/v1/checkouts/" . $issue1->issue_id . "/renewal");
191
192 my $renewal = $issue1->renewals->last;
193 is( $renewal->renewal_type, 'Manual', 'Manual renewal recorded' );
194
195 $t->get_ok ( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewals" )
196   ->status_is(200)
197   ->json_is('/0/checkout_id' => $issue1->issue_id)
198   ->json_is('/0/interface'   => 'api')
199   ->json_is('/0/renewer_id'  => $librarian->borrowernumber );
200
201 $t->post_ok( "//$unauth_userid:$unauth_password@/api/v1/checkouts/" . $issue3->issue_id . "/renewal" )
202   ->status_is(403)
203   ->json_is({ error => "Authorization failure. Missing required permission(s).",
204               required_permissions => { circulate => "circulate_remaining_permissions" }
205             });
206
207 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
208   ->status_is(200)
209   ->json_is({
210         allows_renewal   => Mojo::JSON->true,
211         max_renewals     => 1,
212         unseen_renewals  => 0,
213         current_renewals => 0,
214         error            => undef
215     });
216
217 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/renewal" )
218   ->status_is(201)
219   ->json_is('/due_date' => output_pref({ dateformat => "rfc3339", dt => $expected_datedue}) )
220   ->header_is(Location => "/api/v1/checkouts/" . $issue2->issue_id . "/renewal");
221
222
223 $t->post_ok( "//$userid:$password@/api/v1/checkouts/" . $issue1->issue_id . "/renewal" )
224   ->status_is(403)
225   ->json_is({ error => 'Renewal not authorized (too_many)' });
226
227 $t->get_ok( "//$userid:$password@/api/v1/checkouts/" . $issue2->issue_id . "/allows_renewal")
228   ->status_is(200)
229   ->json_is({
230         allows_renewal   => Mojo::JSON->false,
231         max_renewals     => 1,
232         unseen_renewals  => 0,
233         current_renewals => 1,
234         error            => 'too_many'
235     });
236
237 $schema->storage->txn_rollback;
238
239 subtest 'get_availability' => sub {
240
241     plan tests => 28;
242
243     $schema->storage->txn_begin;
244     my $librarian = $builder->build_object(
245         {
246             class => 'Koha::Patrons',
247             value => { flags => 2 }
248         }
249     );
250     my $password = 'thePassword123';
251     $librarian->set_password( { password => $password, skip_validation => 1 } );
252     my $userid = $librarian->userid;
253
254     my $patron = $builder->build_object(
255         {
256             class => 'Koha::Patrons',
257             value => { flags => 0 }
258         }
259     );
260     my $unauth_password = 'thePassword000';
261     $patron->set_password(
262         { password => $unauth_password, skip_validattion => 1 } );
263     my $unauth_userid = $patron->userid;
264     my $patron_id     = $patron->borrowernumber;
265
266     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
267
268     my $item1    = $builder->build_sample_item;
269     my $item1_id = $item1->id;
270
271     my %issuingimpossible = ();
272     my %needsconfirmation = ();
273     my %alerts            = ();
274     my %messages          = ();
275     my $mocked_circ = Test::MockModule->new('C4::Circulation');
276     $mocked_circ->mock(
277         'CanBookBeIssued',
278         sub {
279             return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
280         }
281     );
282
283     $t->get_ok(
284 "//$unauth_userid:$unauth_password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
285     )->status_is(403)->json_is(
286         {
287             error => "Authorization failure. Missing required permission(s).",
288             required_permissions =>
289               { circulate => "circulate_remaining_permissions" }
290         }
291     );
292
293     # Available
294     $t->get_ok(
295 "//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
296     )->status_is(200)->json_is( '/blockers' => {} )
297       ->json_is( '/confirms'           => {} )->json_is( '/warnings' => {} )
298       ->json_has( '/confirmation_token');
299
300     # Blocked
301     %issuingimpossible = ( GNA => 1 );
302     $t->get_ok(
303 "//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
304     )->status_is(200)->json_is( '/blockers' => { GNA => 1 } )
305       ->json_is( '/confirms'           => {} )->json_is( '/warnings' => {} )
306       ->json_has( '/confirmation_token');
307     %issuingimpossible = ();
308
309     # Warnings/Info
310     %alerts   = ( alert1   => "this is an alert" );
311     %messages = ( message1 => "this is a message" );
312     $t->get_ok(
313 "//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
314     )->status_is(200)->json_is( '/blockers' => {} )
315       ->json_is( '/confirms' => {} )
316       ->json_is( '/warnings' =>
317           { alert1 => "this is an alert", message1 => "this is a message" } )
318       ->json_has( '/confirmation_token');
319     %alerts   = ();
320     %messages = ();
321
322     # Needs confirm
323     %needsconfirmation = ( confirm1 => 1, confirm2 => 'please' );
324     my $token = Koha::Token->new->generate_jwt( { id => $librarian->id . ":" . $item1_id . ":confirm1:confirm2:please" });
325     $t->get_ok(
326 "//$userid:$password@/api/v1/checkouts/availability?item_id=$item1_id&patron_id=$patron_id"
327     )->status_is(200)->json_is( '/blockers' => {} )
328       ->json_is( '/confirms' => { confirm1 => 1, confirm2 => 'please' } )
329       ->json_is( '/warnings' => {} )
330       ->json_has( '/confirmation_token');
331     my $confirmation_token = $t->tx->res->json('/confirmation_token');
332     ok(
333         Koha::Token->new->check_jwt(
334             {
335                 id => $librarian->id . ":"
336                   . $item1_id
337                   . ":confirm1:confirm2:please",
338                 token => $confirmation_token
339             }
340         ),
341         'Correct token'
342     );
343
344     $schema->storage->txn_rollback;
345 };
346
347 subtest 'add checkout' => sub {
348
349     plan tests => 9;
350
351     $schema->storage->txn_begin;
352     my $librarian = $builder->build_object(
353         {
354             class => 'Koha::Patrons',
355             value => { flags => 2 }
356         }
357     );
358     my $password = 'thePassword123';
359     $librarian->set_password( { password => $password, skip_validation => 1 } );
360     my $userid = $librarian->userid;
361
362     my $patron = $builder->build_object(
363         {
364             class => 'Koha::Patrons',
365             value => { flags => 0 }
366         }
367     );
368     my $unauth_password = 'thePassword000';
369     $patron->set_password(
370         { password => $unauth_password, skip_validattion => 1 } );
371     my $unauth_userid = $patron->userid;
372     my $patron_id     = $patron->borrowernumber;
373
374     my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode};
375
376     my $item1    = $builder->build_sample_item;
377     my $item1_id = $item1->id;
378
379     my %issuingimpossible = ();
380     my %needsconfirmation = ();
381     my %alerts            = ();
382     my %messages          = ();
383     my $mocked_circ = Test::MockModule->new('C4::Circulation');
384     $mocked_circ->mock(
385         'CanBookBeIssued',
386         sub {
387             return ( \%issuingimpossible, \%needsconfirmation, \%alerts, \%messages );
388         }
389     );
390
391     $t->post_ok(
392         "//$unauth_userid:$unauth_password@/api/v1/checkouts" => json =>
393           { item_id => $item1_id, patron_id => $patron_id } )->status_is(403)
394       ->json_is(
395         {
396             error => "Authorization failure. Missing required permission(s).",
397             required_permissions =>
398               { circulate => "circulate_remaining_permissions" }
399         }
400       );
401
402     $t->post_ok( "//$userid:$password@/api/v1/checkouts" => json =>
403           { item_id => $item1_id, patron_id => $patron_id } )->status_is(201);
404
405     # Needs confirm
406     %needsconfirmation = ( confirm1 => 1, confirm2 => 'please' );
407     $t->post_ok(
408         "//$userid:$password@/api/v1/checkouts" => json => {
409             item_id            => $item1_id,
410             patron_id          => $patron_id,
411         }
412     )->status_is(412);
413
414     my $token = Koha::Token->new->generate_jwt(
415         {
416                 id => $librarian->id . ":"
417               . $item1_id
418               . ":confirm1:confirm2:please"
419         }
420     );
421     $t->post_ok(
422         "//$userid:$password@/api/v1/checkouts?confirmation=$token" => json => {
423             item_id            => $item1_id,
424             patron_id          => $patron_id
425         }
426     )->status_is(201)->or(sub { diag $t->tx->res->body });
427     %needsconfirmation = ();
428
429     $schema->storage->txn_rollback;
430 };