Bug 28772: Fix auth_authenticate_api_request.t
[koha.git] / t / db_dependent / api / v1 / patrons.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 => 7;
21 use Test::MockModule;
22 use Test::Mojo;
23 use Test::Warn;
24
25 use t::lib::TestBuilder;
26 use t::lib::Mocks;
27 use t::lib::Dates;
28
29 use C4::Auth;
30 use Koha::Database;
31 use Koha::Exceptions::Patron;
32 use Koha::Exceptions::Patron::Attribute;
33 use Koha::Patron::Attributes;
34 use Koha::Patron::Debarments qw/AddDebarment/;
35
36 my $schema  = Koha::Database->new->schema;
37 my $builder = t::lib::TestBuilder->new;
38
39 my $t = Test::Mojo->new('Koha::REST::V1');
40 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
41
42 subtest 'list() tests' => sub {
43     plan tests => 2;
44
45     $schema->storage->txn_begin;
46     unauthorized_access_tests('GET', undef, undef);
47     $schema->storage->txn_rollback;
48
49     subtest 'librarian access tests' => sub {
50         plan tests => 16;
51
52         $schema->storage->txn_begin;
53
54         my $librarian = $builder->build_object(
55             {
56                 class => 'Koha::Patrons',
57                 value => { flags => 2**4 }    # borrowers flag = 4
58             }
59         );
60         my $password = 'thePassword123';
61         $librarian->set_password( { password => $password, skip_validation => 1 } );
62         my $userid = $librarian->userid;
63
64         $t->get_ok("//$userid:$password@/api/v1/patrons")
65           ->status_is(200);
66
67         $t->get_ok("//$userid:$password@/api/v1/patrons?cardnumber=" . $librarian->cardnumber)
68           ->status_is(200)
69           ->json_is('/0/cardnumber' => $librarian->cardnumber);
70
71         $t->get_ok("//$userid:$password@/api/v1/patrons?q={\"cardnumber\":\"" . $librarian->cardnumber ."\"}")
72           ->status_is(200)
73           ->json_is('/0/cardnumber' => $librarian->cardnumber);
74
75         $t->get_ok("//$userid:$password@/api/v1/patrons?address2=" . $librarian->address2)
76           ->status_is(200)
77           ->json_is('/0/address2' => $librarian->address2);
78
79         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
80         AddDebarment({ borrowernumber => $patron->borrowernumber });
81
82         $t->get_ok("//$userid:$password@/api/v1/patrons?restricted=" . Mojo::JSON->true . "&cardnumber=" . $patron->cardnumber )
83           ->status_is(200)
84           ->json_has('/0/restricted')
85           ->json_is( '/0/restricted' => Mojo::JSON->true )
86           ->json_hasnt('/1');
87
88         $schema->storage->txn_rollback;
89     };
90 };
91
92 subtest 'get() tests' => sub {
93     plan tests => 2;
94
95     $schema->storage->txn_begin;
96     unauthorized_access_tests('GET', -1, undef);
97     $schema->storage->txn_rollback;
98
99     subtest 'librarian access tests' => sub {
100         plan tests => 6;
101
102         $schema->storage->txn_begin;
103
104         my $librarian = $builder->build_object(
105             {
106                 class => 'Koha::Patrons',
107                 value => { flags => 2**4 }    # borrowers flag = 4
108             }
109         );
110         my $password = 'thePassword123';
111         $librarian->set_password( { password => $password, skip_validation => 1 } );
112         my $userid = $librarian->userid;
113
114         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
115
116         $t->get_ok("//$userid:$password@/api/v1/patrons/" . $patron->id)
117           ->status_is(200)
118           ->json_is('/patron_id'        => $patron->id)
119           ->json_is('/category_id'      => $patron->categorycode )
120           ->json_is('/surname'          => $patron->surname)
121           ->json_is('/patron_card_lost' => Mojo::JSON->false );
122
123         $schema->storage->txn_rollback;
124     };
125 };
126
127 subtest 'add() tests' => sub {
128     plan tests => 2;
129
130     $schema->storage->txn_begin;
131
132     my $patron = $builder->build_object( { class => 'Koha::Patrons' } )->to_api;
133
134     unauthorized_access_tests('POST', undef, $patron);
135
136     $schema->storage->txn_rollback;
137
138     subtest 'librarian access tests' => sub {
139         plan tests => 22;
140
141         $schema->storage->txn_begin;
142
143         my $extended_attrs_exception;
144         my $type = 'hey';
145         my $code = 'ho';
146         my $attr = "Let's go";
147
148         # Mock early, so existing mandatory attributes don't break all the tests
149         my $mocked_patron = Test::MockModule->new('Koha::Patron');
150         $mocked_patron->mock(
151             'extended_attributes',
152             sub {
153
154                 if ($extended_attrs_exception) {
155                     if ( $extended_attrs_exception eq 'Koha::Exceptions::Patron::Attribute::NonRepeatable'
156                         or $extended_attrs_exception eq 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint'
157                       )
158                     {
159                         $extended_attrs_exception->throw(
160                             attribute => Koha::Patron::Attribute->new(
161                                 { code => $code, attribute => $attr }
162                             )
163                         );
164                     }
165                     else {
166                         $extended_attrs_exception->throw( type => $type );
167                     }
168                 }
169                 return [];
170             }
171         );
172
173         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
174         my $newpatron = $patron->to_api;
175         # delete RO attributes
176         delete $newpatron->{patron_id};
177         delete $newpatron->{restricted};
178         delete $newpatron->{anonymized};
179
180         # Create a library just to make sure its ID doesn't exist on the DB
181         my $library_to_delete = $builder->build_object({ class => 'Koha::Libraries' });
182         my $deleted_library_id = $library_to_delete->id;
183         # Delete library
184         $library_to_delete->delete;
185
186         my $librarian = $builder->build_object(
187             {
188                 class => 'Koha::Patrons',
189                 value => { flags => 2**4 }    # borrowers flag = 4
190             }
191         );
192         my $password = 'thePassword123';
193         $librarian->set_password( { password => $password, skip_validation => 1 } );
194         my $userid = $librarian->userid;
195
196         $newpatron->{library_id} = $deleted_library_id;
197
198         warning_like {
199             $t->post_ok("//$userid:$password@/api/v1/patrons" => json => $newpatron)
200               ->status_is(409)
201               ->json_is('/error' => "Duplicate ID"); }
202             qr/DBD::mysql::st execute failed: Duplicate entry/;
203
204         $newpatron->{library_id} = $patron->branchcode;
205
206         # Create a library just to make sure its ID doesn't exist on the DB
207         my $category_to_delete = $builder->build_object({ class => 'Koha::Patron::Categories' });
208         my $deleted_category_id = $category_to_delete->id;
209         # Delete library
210         $category_to_delete->delete;
211
212         $newpatron->{category_id} = $deleted_category_id; # Test invalid patron category
213
214         $t->post_ok("//$userid:$password@/api/v1/patrons" => json => $newpatron)
215           ->status_is(400)
216           ->json_is('/error' => "Given category_id does not exist");
217         $newpatron->{category_id} = $patron->categorycode;
218
219         $newpatron->{falseproperty} = "Non existent property";
220
221         $t->post_ok("//$userid:$password@/api/v1/patrons" => json => $newpatron)
222           ->status_is(400);
223
224         delete $newpatron->{falseproperty};
225
226         my $patron_to_delete = $builder->build_object({ class => 'Koha::Patrons' });
227         $newpatron = $patron_to_delete->to_api;
228         # delete RO attributes
229         delete $newpatron->{patron_id};
230         delete $newpatron->{restricted};
231         delete $newpatron->{anonymized};
232         $patron_to_delete->delete;
233
234         $t->post_ok("//$userid:$password@/api/v1/patrons" => json => $newpatron)
235           ->status_is(201, 'Patron created successfully')
236           ->header_like(
237             Location => qr|^\/api\/v1\/patrons/\d*|,
238             'SWAGGER3.4.1'
239           )
240           ->json_has('/patron_id', 'got a patron_id')
241           ->json_is( '/cardnumber' => $newpatron->{ cardnumber })
242           ->json_is( '/surname'    => $newpatron->{ surname })
243           ->json_is( '/firstname'  => $newpatron->{ firstname });
244
245         warning_like {
246             $t->post_ok("//$userid:$password@/api/v1/patrons" => json => $newpatron)
247               ->status_is(409)
248               ->json_has( '/error', 'Fails when trying to POST duplicate cardnumber' )
249               ->json_like( '/conflict' => qr/(borrowers\.)?cardnumber/ ); }
250             qr/DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(borrowers\.)?cardnumber'/;
251
252         subtest 'extended_attributes handling tests' => sub {
253
254             plan tests => 19;
255
256             my $patrons_count = Koha::Patrons->search->count;
257
258             $extended_attrs_exception = 'Koha::Exceptions::Patron::MissingMandatoryExtendedAttribute';
259             $t->post_ok(
260                 "//$userid:$password@/api/v1/patrons" => json => {
261                     "firstname"   => "Katrina",
262                     "surname"     => "Fischer",
263                     "address"     => "Somewhere",
264                     "category_id" => "ST",
265                     "city"        => "Konstanz",
266                     "library_id"  => "MPL"
267                 }
268             )->status_is(400)
269               ->json_is( '/error' =>
270                   "Missing mandatory extended attribute (type=$type)" );
271
272             is( Koha::Patrons->search->count, $patrons_count, 'No patron added' );
273
274             $extended_attrs_exception = 'Koha::Exceptions::Patron::Attribute::InvalidType';
275             $t->post_ok(
276                 "//$userid:$password@/api/v1/patrons" => json => {
277                     "firstname"   => "Katrina",
278                     "surname"     => "Fischer",
279                     "address"     => "Somewhere",
280                     "category_id" => "ST",
281                     "city"        => "Konstanz",
282                     "library_id"  => "MPL"
283                 }
284             )->status_is(400)
285               ->json_is( '/error' =>
286                   "Tried to use an invalid attribute type. type=$type" );
287
288             is( Koha::Patrons->search->count, $patrons_count, 'No patron added' );
289
290             $extended_attrs_exception = 'Koha::Exceptions::Patron::Attribute::NonRepeatable';
291             $t->post_ok(
292                 "//$userid:$password@/api/v1/patrons" => json => {
293                     "firstname"   => "Katrina",
294                     "surname"     => "Fischer",
295                     "address"     => "Somewhere",
296                     "category_id" => "ST",
297                     "city"        => "Konstanz",
298                     "library_id"  => "MPL"
299                 }
300             )->status_is(400)
301               ->json_is( '/error' =>
302                   "Tried to add more than one non-repeatable attributes. type=$code value=$attr" );
303
304             is( Koha::Patrons->search->count, $patrons_count, 'No patron added' );
305
306             $extended_attrs_exception = 'Koha::Exceptions::Patron::Attribute::UniqueIDConstraint';
307             $t->post_ok(
308                 "//$userid:$password@/api/v1/patrons" => json => {
309                     "firstname"   => "Katrina",
310                     "surname"     => "Fischer",
311                     "address"     => "Somewhere",
312                     "category_id" => "ST",
313                     "city"        => "Konstanz",
314                     "library_id"  => "MPL"
315                 }
316             )->status_is(400)
317               ->json_is( '/error' =>
318                   "Your action breaks a unique constraint on the attribute. type=$code value=$attr" );
319
320             is( Koha::Patrons->search->count, $patrons_count, 'No patron added' );
321
322             $mocked_patron->unmock('extended_attributes');
323             # Temporarily get rid of mandatory attribute types
324             Koha::Patron::Attribute::Types->search({ mandatory => 1 })->delete;
325             # Create a couple attribute attribute types
326             my $repeatable_1 = $builder->build_object(
327                 {
328                     class => 'Koha::Patron::Attribute::Types',
329                     value => {
330                         mandatory     => 0,
331                         repeatable    => 1,
332                         unique        => 0,
333                         category_code => 'ST'
334                     }
335                 }
336             );
337             my $repeatable_2 = $builder->build_object(
338                 {
339                     class => 'Koha::Patron::Attribute::Types',
340                     value => {
341                         mandatory     => 0,
342                         repeatable    => 1,
343                         unique        => 0,
344                         category_code => 'ST'
345                     }
346                 }
347             );
348
349             my $patron_id = $t->post_ok(
350                 "//$userid:$password@/api/v1/patrons" => json => {
351                     "firstname"   => "Katrina",
352                     "surname"     => "Fischer",
353                     "address"     => "Somewhere",
354                     "category_id" => "ST",
355                     "city"        => "Konstanz",
356                     "library_id"  => "MPL",
357                     "extended_attributes" => [
358                         { type => $repeatable_1->code, value => 'a' },
359                         { type => $repeatable_1->code, value => 'b' },
360                         { type => $repeatable_1->code, value => 'c' },
361                         { type => $repeatable_2->code, value => 'd' },
362                         { type => $repeatable_2->code, value => 'e' }
363                     ]
364                 }
365             )->status_is(201, 'Patron added')->tx->res->json->{patron_id};
366             my $extended_attributes = join( ' ', sort map {$_->attribute} Koha::Patrons->find($patron_id)->extended_attributes->as_list);
367             is( $extended_attributes, 'a b c d e', 'Extended attributes are stored correctly');
368         };
369
370         $schema->storage->txn_rollback;
371     };
372 };
373
374 subtest 'update() tests' => sub {
375     plan tests => 2;
376
377     $schema->storage->txn_begin;
378     unauthorized_access_tests('PUT', 123, {email => 'nobody@example.com'});
379     $schema->storage->txn_rollback;
380
381     subtest 'librarian access tests' => sub {
382         plan tests => 42;
383
384         $schema->storage->txn_begin;
385
386         my $authorized_patron = $builder->build_object(
387             {
388                 class => 'Koha::Patrons',
389                 value => { flags => 1 }
390             }
391         );
392         my $password = 'thePassword123';
393         $authorized_patron->set_password(
394             { password => $password, skip_validation => 1 } );
395         my $userid = $authorized_patron->userid;
396
397         my $unauthorized_patron = $builder->build_object(
398             {
399                 class => 'Koha::Patrons',
400                 value => { flags => 0 }
401             }
402         );
403         $unauthorized_patron->set_password( { password => $password, skip_validation => 1 } );
404         my $unauth_userid = $unauthorized_patron->userid;
405
406         my $patron_1  = $authorized_patron;
407         my $patron_2  = $unauthorized_patron;
408         my $newpatron = $unauthorized_patron->to_api;
409         # delete RO attributes
410         delete $newpatron->{patron_id};
411         delete $newpatron->{restricted};
412         delete $newpatron->{anonymized};
413
414         $t->put_ok("//$userid:$password@/api/v1/patrons/-1" => json => $newpatron)
415           ->status_is(404)
416           ->json_has('/error', 'Fails when trying to PUT nonexistent patron');
417
418         # Create a library just to make sure its ID doesn't exist on the DB
419         my $category_to_delete = $builder->build_object({ class => 'Koha::Patron::Categories' });
420         my $deleted_category_id = $category_to_delete->id;
421         # Delete library
422         $category_to_delete->delete;
423
424         # Use an invalid category
425         $newpatron->{category_id} = $deleted_category_id;
426
427         $t->put_ok("//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron)
428           ->status_is(400)
429           ->json_is('/error' => "Given category_id does not exist");
430
431         # Restore the valid category
432         $newpatron->{category_id} = $patron_2->categorycode;
433
434         # Create a library just to make sure its ID doesn't exist on the DB
435         my $library_to_delete = $builder->build_object({ class => 'Koha::Libraries' });
436         my $deleted_library_id = $library_to_delete->id;
437         # Delete library
438         $library_to_delete->delete;
439
440         # Use an invalid library_id
441         $newpatron->{library_id} = $deleted_library_id;
442
443         warning_like {
444             $t->put_ok("//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron)
445               ->status_is(400)
446               ->json_is('/error' => "Given library_id does not exist"); }
447             qr/DBD::mysql::st execute failed: Cannot add or update a child row: a foreign key constraint fails/;
448
449         # Restore the valid library_id
450         $newpatron->{library_id} = $patron_2->branchcode;
451
452         # Use an invalid attribute
453         $newpatron->{falseproperty} = "Non existent property";
454
455         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
456           ->status_is(400)
457           ->json_is('/errors/0/message' =>
458                     'Properties not allowed: falseproperty.');
459
460         # Get rid of the invalid attribute
461         delete $newpatron->{falseproperty};
462
463         # Set both cardnumber and userid to already existing values
464         $newpatron->{cardnumber} = $patron_1->cardnumber;
465         $newpatron->{userid}     = $patron_1->userid;
466
467         warning_like {
468             $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
469               ->status_is(409)
470               ->json_has( '/error', "Fails when trying to update to an existing cardnumber or userid")
471               ->json_like( '/conflict' => qr/(borrowers\.)?cardnumber/ ); }
472             qr/DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(borrowers\.)?cardnumber'/;
473
474         $newpatron->{ cardnumber } = $patron_1->id . $patron_2->id;
475         $newpatron->{ userid }     = "user" . $patron_1->id.$patron_2->id;
476         $newpatron->{ surname }    = "user" . $patron_1->id.$patron_2->id;
477
478         my $result = $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
479           ->status_is(200, 'Patron updated successfully');
480
481         # Put back the RO attributes
482         $newpatron->{patron_id} = $unauthorized_patron->to_api->{patron_id};
483         $newpatron->{restricted} = $unauthorized_patron->to_api->{restricted};
484         $newpatron->{anonymized} = $unauthorized_patron->to_api->{anonymized};
485
486         my $got = $result->tx->res->json;
487         my $updated_on_got = delete $got->{updated_on};
488         my $updated_on_expected = delete $newpatron->{updated_on};
489         is_deeply($got, $newpatron, 'Returned patron from update matches expected');
490         t::lib::Dates::compare( $updated_on_got, $updated_on_expected, 'updated_on values matched' );
491
492
493         is(Koha::Patrons->find( $patron_2->id )->cardnumber,
494            $newpatron->{ cardnumber }, 'Patron is really updated!');
495
496         my $superlibrarian = $builder->build_object(
497             {
498                 class => 'Koha::Patrons',
499                 value => { flags => 1 }
500             }
501         );
502
503         $newpatron->{cardnumber} = $superlibrarian->cardnumber;
504         $newpatron->{userid}     = $superlibrarian->userid;
505         $newpatron->{email}      = 'nosense@no.no';
506         # delete RO attributes
507         delete $newpatron->{patron_id};
508         delete $newpatron->{restricted};
509         delete $newpatron->{anonymized};
510
511         # attempt to update
512         $authorized_patron->flags( 2**4 )->store; # borrowers flag = 4
513         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
514           ->status_is(403, "Non-superlibrarian user change of superlibrarian email forbidden")
515           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
516
517         # attempt to unset
518         $newpatron->{email} = undef;
519         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
520           ->status_is(403, "Non-superlibrarian user change of superlibrarian email to undefined forbidden")
521           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
522
523         $newpatron->{email}           = $superlibrarian->email;
524         $newpatron->{secondary_email} = 'nonsense@no.no';
525
526         # attempt to update
527         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
528           ->status_is(403, "Non-superlibrarian user change of superlibrarian secondary_email forbidden")
529           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
530
531         # attempt to unset
532         $newpatron->{secondary_email} = undef;
533         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
534           ->status_is(403, "Non-superlibrarian user change of superlibrarian secondary_email to undefined forbidden")
535           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
536
537         $newpatron->{secondary_email}  = $superlibrarian->emailpro;
538         $newpatron->{altaddress_email} = 'nonsense@no.no';
539
540         # attempt to update
541         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
542           ->status_is(403, "Non-superlibrarian user change of superlibrarian altaddress_email forbidden")
543           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
544
545         # attempt to unset
546         $newpatron->{altaddress_email} = undef;
547         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
548           ->status_is(403, "Non-superlibrarian user change of superlibrarian altaddress_email to undefined forbidden")
549           ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
550
551         # update patron without sending email
552         delete $newpatron->{email};
553         delete $newpatron->{secondary_email};
554         delete $newpatron->{altaddress_email};
555         $t->put_ok( "//$userid:$password@/api/v1/patrons/" . $superlibrarian->borrowernumber => json => $newpatron )
556           ->status_is(200, "Non-superlibrarian user can edit superlibrarian successfully if not changing email");
557 #  ->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
558
559         $schema->storage->txn_rollback;
560     };
561 };
562
563 subtest 'delete() tests' => sub {
564     plan tests => 2;
565
566     $schema->storage->txn_begin;
567     unauthorized_access_tests('DELETE', 123, undef);
568     $schema->storage->txn_rollback;
569
570     subtest 'librarian access test' => sub {
571         plan tests => 8;
572
573         $schema->storage->txn_begin;
574
575         my $authorized_patron = $builder->build_object(
576             {
577                 class => 'Koha::Patrons',
578                 value => { flags => 2**4 }    # borrowers flag = 4
579             }
580         );
581         my $password = 'thePassword123';
582         $authorized_patron->set_password(
583             { password => $password, skip_validation => 1 } );
584         my $userid = $authorized_patron->userid;
585
586         $t->delete_ok("//$userid:$password@/api/v1/patrons/-1")
587           ->status_is(404, 'Patron not found');
588
589         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
590
591         t::lib::Mocks::mock_preference('AnonymousPatron', $patron->borrowernumber);
592         $t->delete_ok("//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber)
593           ->status_is(403, 'Anonymous patron cannot be deleted')
594           ->json_is( { error => 'Anonymous patron cannot be deleted' } );
595
596         t::lib::Mocks::mock_preference('AnonymousPatron', 0); # back to default
597         $t->delete_ok("//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber)
598           ->status_is(204, 'SWAGGER3.2.4')
599           ->content_is('', 'SWAGGER3.3.4');
600
601         $schema->storage->txn_rollback;
602     };
603 };
604
605 subtest 'guarantors_can_see_charges() tests' => sub {
606
607     plan tests => 11;
608
609     t::lib::Mocks::mock_preference( 'RESTPublicAPI', 1 );
610     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
611
612     $schema->storage->txn_begin;
613
614     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy_guarantor_fines => 0 } });
615     my $password = 'thePassword123';
616     $patron->set_password({ password => $password, skip_validation => 1 });
617     my $userid = $patron->userid;
618     my $patron_id = $patron->borrowernumber;
619
620     t::lib::Mocks::mock_preference( 'AllowPatronToSetFinesVisibilityForGuarantor', 0 );
621
622     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_charges" => json => { allowed => Mojo::JSON->true } )
623       ->status_is( 403 )
624       ->json_is( '/error', 'The current configuration doesn\'t allow the requested action.' );
625
626     t::lib::Mocks::mock_preference( 'AllowPatronToSetFinesVisibilityForGuarantor', 1 );
627
628     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_charges" => json => { allowed => Mojo::JSON->true } )
629       ->status_is( 200 )
630       ->json_is( {} );
631
632     ok( $patron->discard_changes->privacy_guarantor_fines, 'privacy_guarantor_fines has been set correctly' );
633
634     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_charges" => json => { allowed => Mojo::JSON->false } )
635       ->status_is( 200 )
636       ->json_is( {} );
637
638     ok( !$patron->discard_changes->privacy_guarantor_fines, 'privacy_guarantor_fines has been set correctly' );
639
640     $schema->storage->txn_rollback;
641 };
642
643 subtest 'guarantors_can_see_checkouts() tests' => sub {
644
645     plan tests => 11;
646
647     t::lib::Mocks::mock_preference( 'RESTPublicAPI', 1 );
648     t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
649
650     $schema->storage->txn_begin;
651
652     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy_guarantor_checkouts => 0 } });
653     my $password = 'thePassword123';
654     $patron->set_password({ password => $password, skip_validation => 1 });
655     my $userid = $patron->userid;
656     my $patron_id = $patron->borrowernumber;
657
658     t::lib::Mocks::mock_preference( 'AllowPatronToSetCheckoutsVisibilityForGuarantor', 0 );
659
660     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_checkouts" => json => { allowed => Mojo::JSON->true } )
661       ->status_is( 403 )
662       ->json_is( '/error', 'The current configuration doesn\'t allow the requested action.' );
663
664     t::lib::Mocks::mock_preference( 'AllowPatronToSetCheckoutsVisibilityForGuarantor', 1 );
665
666     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_checkouts" => json => { allowed => Mojo::JSON->true } )
667       ->status_is( 200 )
668       ->json_is( {} );
669
670     ok( $patron->discard_changes->privacy_guarantor_checkouts, 'privacy_guarantor_checkouts has been set correctly' );
671
672     $t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_checkouts" => json => { allowed => Mojo::JSON->false } )
673       ->status_is( 200 )
674       ->json_is( {} );
675
676     ok( !$patron->discard_changes->privacy_guarantor_checkouts, 'privacy_guarantor_checkouts has been set correctly' );
677
678     $schema->storage->txn_rollback;
679 };
680
681 # Centralized tests for 401s and 403s assuming the endpoint requires
682 # borrowers flag for access
683 sub unauthorized_access_tests {
684     my ($verb, $patron_id, $json) = @_;
685
686     my $endpoint = '/api/v1/patrons';
687     $endpoint .= ($patron_id) ? "/$patron_id" : '';
688
689     subtest 'unauthorized access tests' => sub {
690         plan tests => 5;
691
692         my $verb_ok = lc($verb) . '_ok';
693
694         $t->$verb_ok($endpoint => json => $json)
695           ->status_is(401);
696
697         my $unauthorized_patron = $builder->build_object(
698             {
699                 class => 'Koha::Patrons',
700                 value => { flags => 0 }
701             }
702         );
703         my $password = "thePassword123!";
704         $unauthorized_patron->set_password(
705             { password => $password, skip_validation => 1 } );
706         my $unauth_userid = $unauthorized_patron->userid;
707
708         $t->$verb_ok( "//$unauth_userid:$password\@$endpoint" => json => $json )
709           ->status_is(403)
710           ->json_has('/required_permissions');
711     };
712 }