Bug 26355: Add unit tests

Signed-off-by: Christopher Brannon <cbrannon@cdalibrary.org>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Matt Blenkinsop 2026-02-19 13:34:12 +00:00 committed by Lucas Gass
parent d04923b3b9
commit a76eb18bed
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
5 changed files with 476 additions and 130 deletions

View File

@ -121,6 +121,7 @@ sub submit {
$response->{confirmation_sent} = 1 if $result->{sent};
}
$c->res->headers->location( $c->req->url->to_string );
return $c->render(
status => 201,
openapi => $response

View File

@ -19,7 +19,7 @@
use Modern::Perl;
use Test::More tests => 48;
use Test::More tests => 50;
use Test::NoWarnings;
use Test::Exception;
use Test::Warn;
@ -722,7 +722,7 @@ subtest 'messaging_preferences() tests' => sub {
subtest 'to_api() tests' => sub {
plan tests => 6;
plan tests => 7;
$schema->storage->txn_begin;
@ -760,6 +760,9 @@ subtest 'to_api() tests' => sub {
ok( exists $patron_json->{algo} );
is( $patron_json->{algo}, 'algo' );
my $patron_eligible = $patron->to_api( { user => $consumer } );
ok( exists $patron_eligible->{self_renewal_available} );
$schema->storage->txn_rollback;
};
@ -3799,3 +3802,142 @@ subtest 'fixup_cardnumber and autoMemberNumValue counter' => sub {
$schema->storage->txn_rollback;
};
subtest "is_eligible_for_self_renewal" => sub {
plan tests => 8;
$schema->storage->txn_begin;
my $category = $builder->build_object(
{
class => 'Koha::Patron::Categories',
value => {
self_renewal_enabled => 0, self_renewal_availability_start => 10, self_renewal_if_expired => 10,
self_renewal_fines_block => 10, noissuescharge => 10
}
}
);
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { categorycode => $category->categorycode, dateexpiry => dt_from_string(), debarred => undef }
}
);
is( $patron->is_eligible_for_self_renewal, 0, "Category not enabled" );
$category->self_renewal_enabled(1)->store();
$patron->delete()->store()->discard_changes();
is( $patron->is_eligible_for_self_renewal, 1, "Category now enabled" );
$patron->debarred('2026-01-01')->store;
is( $patron->is_eligible_for_self_renewal, 0, "Patron debarred" );
$patron->debarred(undef)->store;
$patron->delete()->store()->discard_changes();
# Move expiry date outside the window
$patron->dateexpiry( dt_from_string()->add( days => 11 ) )->store;
is( $patron->is_eligible_for_self_renewal, 0, "Patron not yet within the expiry window" );
# Shift the expiry window to cover new expiry date
$category->self_renewal_availability_start(12)->store();
$patron->delete()->store()->discard_changes();
is( $patron->is_eligible_for_self_renewal, 1, "Patron is back within the expiry window" );
# Shift the date to now already be expired
$patron->dateexpiry( dt_from_string()->subtract( days => 11 ) )->store;
is( $patron->is_eligible_for_self_renewal, 0, "Patron can only self renew within 10 days of expiry" );
# Expand the expiry window
$category->self_renewal_if_expired(12)->store();
$patron->delete()->store()->discard_changes();
is( $patron->is_eligible_for_self_renewal, 1, "Patron is back within the expiry window" );
t::lib::Mocks::mock_preference( 'noissuescharge', 10 );
my $fee1 = $builder->build_object(
{
class => 'Koha::Account::Lines',
value => {
borrowernumber => $patron->borrowernumber,
amountoutstanding => 11,
debit_type_code => 'OVERDUE',
}
}
)->store;
is( $patron->is_eligible_for_self_renewal, 0, "Patron is outside the charge limits" );
$schema->storage->txn_rollback;
};
subtest "request_modification" => sub {
plan tests => 5;
$schema->storage->txn_begin;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
}
);
my $modification_data = { firstname => 'Newname', changed_fields => 'firstname' };
t::lib::Mocks::mock_preference( 'OPACPatronDetails', 1 );
t::lib::Mocks::mock_preference( 'AutoApprovePatronProfileSettings', 0 );
$patron->request_modification($modification_data);
my @modifications = Koha::Patron::Modifications->search( { borrowernumber => $patron->borrowernumber } )->as_list;
is( scalar(@modifications), 1, "New modification has replaced any existing mods" );
my $modification = $modifications[0];
is( $modification->changed_fields, 'firstname', 'Fields correctly set' );
is( $modification->firstname, $modification_data->{firstname}, 'Fields correctly set' );
t::lib::Mocks::mock_preference( 'AutoApprovePatronProfileSettings', 1 );
$patron->request_modification($modification_data);
@modifications = Koha::Patron::Modifications->search( { borrowernumber => $patron->borrowernumber } )->as_list;
is( scalar(@modifications), 0, "New modification has been approved and deleted" );
is( $patron->discard_changes()->firstname, $modification_data->{firstname}, 'Name updated' );
$schema->storage->txn_rollback;
};
subtest "create_expiry_notice_parameters" => sub {
plan tests => 1;
$schema->storage->txn_begin;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { branchcode => $library->branchcode }
}
);
my $expected_return = {
'letter_params' => {
'borrowernumber' => $patron->borrowernumber,
'branchcode' => $library->branchcode,
'tables' => {
'borrowers' => $patron->borrowernumber,
'branches' => $library->branchcode
},
'module' => 'members',
'letter_code' => 'MEMBERSHIP_RENEWED',
'lang' => $patron->lang
},
'message_name' => 'Patron_Expiry',
'forceprint' => 0
};
my $letter_params = $patron->create_expiry_notice_parameters(
{ letter_code => 'MEMBERSHIP_RENEWED', forceprint => 0, is_notice_mandatory => 0 } );
is_deeply( $letter_params, $expected_return, 'Letter params generated correctly' );
$schema->storage->txn_rollback;
};

View File

@ -18,7 +18,7 @@
use Modern::Perl;
use Test::NoWarnings;
use Test::More tests => 2;
use Test::More tests => 3;
use Test::Mojo;
use t::lib::TestBuilder;
@ -66,11 +66,8 @@ subtest 'list() tests' => sub {
$t->get_ok("//$userid:$password@/api/v1/patron_categories")->status_is(200);
$t->get_ok("//$userid:$password@/api/v1/patron_categories?q={\"me.categorycode\":\"TEST\"}")
->status_is(200)
->json_has('/0/name')
->json_is( '/0/name' => 'Test' )
->json_hasnt('/1');
$t->get_ok("//$userid:$password@/api/v1/patron_categories?q={\"me.categorycode\":\"TEST\"}")->status_is(200)
->json_has('/0/name')->json_is( '/0/name' => 'Test' )->json_hasnt('/1');
# Off limits search
@ -95,19 +92,79 @@ subtest 'list() tests' => sub {
my $off_limits_userid = $off_limits_librarian->userid;
$t->get_ok("//$off_limits_userid:$off_limits_password@/api/v1/patron_categories?q={\"me.categorycode\":\"TEST\"}")
->status_is(200)
->json_hasnt('/0');
->status_is(200)->json_hasnt('/0');
# Off limits librarian category has changed to one within limits
$off_limits_librarian->branchcode( $library->branchcode )->store;
$t->get_ok("//$off_limits_userid:$off_limits_password@/api/v1/patron_categories?q={\"me.categorycode\":\"TEST\"}")
->status_is(200)
->json_has('/0/name')
->json_is( [ $category->to_api ] )
->json_hasnt('/1');
->status_is(200)->json_has('/0/name')->json_is( [ $category->to_api ] )->json_hasnt('/1');
$schema->storage->txn_rollback;
};
subtest 'add() tests' => sub {
plan tests => 9;
$schema->storage->txn_begin;
my $library = $builder->build_object( { class => 'Koha::Libraries' } );
my $librarian = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 2**3, categorycode => 'TEST', branchcode => $library->branchcode } # parameters flag = 3
}
);
my $password = 'thePassword123';
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $userid = $librarian->userid;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 0 }
}
);
$patron->set_password( { password => $password, skip_validation => 1 } );
my $unauth_userid = $patron->userid;
my $category = {
patron_category_id => 'new',
name => "A new category",
};
# Unauthorized attempt to write
$t->post_ok( "//$unauth_userid:$password@/api/v1/patron_categories" => json => $category )->status_is(403);
# Authorized attempt to write invalid data
my $category_with_invalid_field = {
blah => "Category Blah",
name => "Won't work"
};
$t->post_ok( "//$userid:$password@/api/v1/patron_categories" => json => $category_with_invalid_field )
->status_is(400)->json_is(
"/errors" => [
{
message => "Properties not allowed: blah.",
path => "/body"
}
]
);
# Authorized attempt to write
my $patron_category_id =
$t->post_ok( "//$userid:$password@/api/v1/patron_categories" => json => $category )
->status_is( 201, 'REST3.2.1' )->header_like(
Location => qr|^\/api\/v1\/patron_categories/\d*|,
'REST3.4.1'
)->json_is( '/name' => $category->{name} )->tx->res->json->{patron_category_id};
$schema->storage->txn_rollback;
};

View File

@ -125,16 +125,13 @@ subtest 'list() tests' => sub {
$t->get_ok("//$userid:$password@/api/v1/patrons")->status_is( 200, 'list_borrowers makes /patrons accessible' );
$t->get_ok( "//$userid:$password@/api/v1/patrons?cardnumber=" . $librarian->cardnumber )
->status_is(200)
$t->get_ok( "//$userid:$password@/api/v1/patrons?cardnumber=" . $librarian->cardnumber )->status_is(200)
->json_is( '/0/cardnumber' => $librarian->cardnumber );
$t->get_ok( "//$userid:$password@/api/v1/patrons?q={\"cardnumber\":\"" . $librarian->cardnumber . "\"}" )
->status_is(200)
->json_is( '/0/cardnumber' => $librarian->cardnumber );
->status_is(200)->json_is( '/0/cardnumber' => $librarian->cardnumber );
$t->get_ok( "//$userid:$password@/api/v1/patrons?address2=" . $librarian->address2 )
->status_is(200)
$t->get_ok( "//$userid:$password@/api/v1/patrons?address2=" . $librarian->address2 )->status_is(200)
->json_is( '/0/address2' => $librarian->address2 );
subtest 'restricted & expired' => sub {
@ -148,19 +145,13 @@ subtest 'list() tests' => sub {
$t->get_ok( "//$userid:$password@/api/v1/patrons?restricted="
. Mojo::JSON->true
. "&cardnumber="
. $patron->cardnumber )
->status_is(200)
->json_has('/0/restricted')
->json_is( '/0/restricted' => Mojo::JSON->true )
->json_has('/0/expired')
->json_is( '/0/expired' => Mojo::JSON->false )
->json_hasnt('/1');
. $patron->cardnumber )->status_is(200)->json_has('/0/restricted')
->json_is( '/0/restricted' => Mojo::JSON->true )->json_has('/0/expired')
->json_is( '/0/expired' => Mojo::JSON->false )->json_hasnt('/1');
$patron->dateexpiry( dt_from_string->subtract( days => 2 ) )->store;
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is(200)
->json_has('/expired')
->json_is( '/expired' => Mojo::JSON->true );
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )->status_is(200)
->json_has('/expired')->json_is( '/expired' => Mojo::JSON->true );
$t->get_ok( "//$userid:$password@/api/v1/patrons?"
. 'q={"extended_attributes.type":"CODE"}' => { 'x-koha-embed' => 'extended_attributes' } )
@ -189,15 +180,13 @@ subtest 'list() tests' => sub {
$t->get_ok( "//$userid:$password@/api/v1/patrons?date_of_birth="
. $date_of_birth
. "&cardnumber="
. $patron->cardnumber )
->status_is(200)
. $patron->cardnumber )->status_is(200)
->json_is( '/0/patron_id' => $patron->id, 'Filtering by date works' );
$t->get_ok( "//$userid:$password@/api/v1/patrons?last_seen="
. $last_seen_rfc3339
. "&cardnumber="
. $patron->cardnumber )
->status_is(200)
. $patron->cardnumber )->status_is(200)
->json_is( '/0/patron_id' => $patron->id, 'Filtering by date-time works' );
my $q = encode_json(
@ -207,8 +196,7 @@ subtest 'list() tests' => sub {
}
);
$t->get_ok("//$userid:$password@/api/v1/patrons?q=$q")
->status_is(200)
$t->get_ok("//$userid:$password@/api/v1/patrons?q=$q")->status_is(200)
->json_is( '/0/patron_id' => $patron->id, 'Filtering by date works' );
$q = encode_json(
@ -218,8 +206,7 @@ subtest 'list() tests' => sub {
}
);
$t->get_ok("//$userid:$password@/api/v1/patrons?q=$q")
->status_is(200)
$t->get_ok("//$userid:$password@/api/v1/patrons?q=$q")->status_is(200)
->json_is( '/0/patron_id' => $patron->id, 'Filtering by date-time works' );
};
@ -263,20 +250,16 @@ subtest 'list() tests' => sub {
my $userid = $librarian->userid;
$t->get_ok( "//$userid:$password@/api/v1/patrons?_order_by=patron_id&q="
. encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )
->status_is(200)
->json_is( '/0/patron_id' => $patron_1->id )
->json_is( '/1/patron_id' => $patron_2->id )
. encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )->status_is(200)
->json_is( '/0/patron_id' => $patron_1->id )->json_is( '/1/patron_id' => $patron_2->id )
->json_is( '/2/patron_id' => $patron_3->id );
@libraries_where_can_see_patrons = ( $library_2->id );
my $res =
$t->get_ok( "//$userid:$password@/api/v1/patrons?_order_by=patron_id&q="
. encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )
->status_is(200)
->json_is( '/0/patron_id' => $patron_3->id, 'Returns the only allowed patron' )
->tx->res->json;
. encode_json( { library_id => [ $library_1->id, $library_2->id ] } ) )->status_is(200)
->json_is( '/0/patron_id' => $patron_3->id, 'Returns the only allowed patron' )->tx->res->json;
is( scalar @{$res}, 1, 'Only one patron returned' );
@ -339,10 +322,8 @@ subtest 'get() tests' => sub {
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron->id )
->status_is( 200, 'list_borrowers permission makes patron visible' )
->json_is( '/patron_id' => $patron->id )
->json_is( '/category_id' => $patron->categorycode )
->json_is( '/surname' => $patron->surname )
->json_is( '/patron_card_lost' => Mojo::JSON->false );
->json_is( '/patron_id' => $patron->id )->json_is( '/category_id' => $patron->categorycode )
->json_is( '/surname' => $patron->surname )->json_is( '/patron_card_lost' => Mojo::JSON->false );
$schema->storage->txn_rollback;
};
@ -384,22 +365,18 @@ subtest 'get() tests' => sub {
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $userid = $librarian->userid;
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_1->id )
->status_is(200)
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_1->id )->status_is(200)
->json_is( '/patron_id' => $patron_1->id );
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->id )
->status_is(200)
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->id )->status_is(200)
->json_is( '/patron_id' => $patron_2->id );
@libraries_where_can_see_patrons = ( $library_1->id );
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_1->id )
->status_is(200)
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_1->id )->status_is(200)
->json_is( '/patron_id' => $patron_1->id );
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->id )
->status_is(404)
$t->get_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->id )->status_is(404)
->json_is( '/error' => 'Patron not found' );
$schema->storage->txn_rollback;
@ -482,14 +459,14 @@ subtest 'add() tests' => sub {
delete $newpatron->{restricted};
delete $newpatron->{expired};
delete $newpatron->{anonymized};
delete $newpatron->{self_renewal_available};
my $password = 'thePassword123';
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $userid = $librarian->userid;
t::lib::Mocks::mock_preference( 'PatronDuplicateMatchingAddFields', 'firstname|surname|dateofbirth' );
$t->post_ok( "//$userid:$password@/api/v1/patrons" => json => $newpatron )
->status_is(409)
$t->post_ok( "//$userid:$password@/api/v1/patrons" => json => $newpatron )->status_is(409)
->json_is( '/error' => "A patron record matching these details already exists" );
# Create a library just to make sure its ID doesn't exist on the DB
@ -503,8 +480,7 @@ subtest 'add() tests' => sub {
# Test duplicate userid constraint
$t->post_ok( "//$userid:$password@/api/v1/patrons" => { 'x-confirm-not-duplicate' => 1 } => json => $newpatron )
->status_is(400)
->json_is( '/error' => "Problem with " . $newpatron->{userid} );
->status_is(400)->json_is( '/error' => "Problem with " . $newpatron->{userid} );
$newpatron->{library_id} = $patron->branchcode;
@ -513,8 +489,7 @@ subtest 'add() tests' => sub {
warning_like {
$t->post_ok(
"//$userid:$password@/api/v1/patrons" => { 'x-confirm-not-duplicate' => 1 } => json => $newpatron )
->status_is(409)
->json_has( '/error', 'Fails when trying to POST duplicate cardnumber' )
->status_is(409)->json_has( '/error', 'Fails when trying to POST duplicate cardnumber' )
->json_like( '/conflict' => qr/(borrowers\.)?cardnumber/ );
}
qr/DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(borrowers\.)?cardnumber'/;
@ -529,8 +504,7 @@ subtest 'add() tests' => sub {
$newpatron->{category_id} = $deleted_category_id; # Test invalid patron category
$t->post_ok( "//$userid:$password@/api/v1/patrons" => { 'x-confirm-not-duplicate' => 1 } => json => $newpatron )
->status_is(400)
->json_is( '/error' => "Given category_id does not exist" );
->status_is(400)->json_is( '/error' => "Given category_id does not exist" );
$newpatron->{category_id} = $patron->categorycode;
$newpatron->{falseproperty} = "Non existent property";
@ -547,6 +521,7 @@ subtest 'add() tests' => sub {
delete $newpatron->{restricted};
delete $newpatron->{expired};
delete $newpatron->{anonymized};
delete $newpatron->{self_renewal_available};
$patron_to_delete->delete;
# Set a date field
@ -560,15 +535,11 @@ subtest 'add() tests' => sub {
$letter_enqueued = 0;
$t->post_ok(
"//$userid:$password@/api/v1/patrons" => { 'x-koha-override' => 'welcome_yes' } => json => $newpatron )
->status_is( 201, 'Patron created successfully' )
->header_like(
->status_is( 201, 'Patron created successfully' )->header_like(
Location => qr|^\/api\/v1\/patrons/\d*|,
'REST3.4.1'
)
->json_has( '/patron_id', 'got a patron_id' )
->json_is( '/cardnumber' => $newpatron->{cardnumber} )
->json_is( '/surname' => $newpatron->{surname} )
->json_is( '/firstname' => $newpatron->{firstname} )
)->json_has( '/patron_id', 'got a patron_id' )->json_is( '/cardnumber' => $newpatron->{cardnumber} )
->json_is( '/surname' => $newpatron->{surname} )->json_is( '/firstname' => $newpatron->{firstname} )
->json_is( '/date_of_birth' => $newpatron->{date_of_birth}, 'Date field set (Bug 28585)' )
->json_is( '/last_seen' => $newpatron->{last_seen}, 'Date-time field set (Bug 28585)' );
@ -800,8 +771,7 @@ subtest 'add() tests' => sub {
"city" => "Konstanz",
"library_id" => "MPL"
}
)
->status_is(400)
)->status_is(400)
->json_is( '/error' => "Tried to add more than one non-repeatable attributes. type=$code value=$attr" );
is( Koha::Patrons->search->count, $patrons_count, 'No patron added' );
@ -816,8 +786,7 @@ subtest 'add() tests' => sub {
"city" => "Konstanz",
"library_id" => "MPL"
}
)
->status_is(400)
)->status_is(400)
->json_is(
'/error' => "Your action breaks a unique constraint on the attribute. type=$code value=$attr" );
@ -978,8 +947,7 @@ subtest 'add() tests' => sub {
"city" => "Bigtown",
"library_id" => "MPL",
}
)
->status_is( 201, 'Patron added with EnhancedMessagingPreferences disabled' )
)->status_is( 201, 'Patron added with EnhancedMessagingPreferences disabled' )
->tx->res->json->{patron_id};
# No messaging preferences should be set
@ -1037,9 +1005,9 @@ subtest 'update() tests' => sub {
delete $newpatron->{restricted};
delete $newpatron->{expired};
delete $newpatron->{anonymized};
delete $newpatron->{self_renewal_available};
$t->put_ok( "//$userid:$password@/api/v1/patrons/-1" => json => $newpatron )
->status_is(404)
$t->put_ok( "//$userid:$password@/api/v1/patrons/-1" => json => $newpatron )->status_is(404)
->json_has( '/error', 'Fails when trying to PUT nonexistent patron' );
# Create a library just to make sure its ID doesn't exist on the DB
@ -1053,8 +1021,7 @@ subtest 'update() tests' => sub {
$newpatron->{category_id} = $deleted_category_id;
$t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
->status_is(400)
->json_is( '/error' => "Given category_id does not exist" );
->status_is(400)->json_is( '/error' => "Given category_id does not exist" );
# Restore the valid category
$newpatron->{category_id} = $patron_2->categorycode;
@ -1071,8 +1038,7 @@ subtest 'update() tests' => sub {
warning_like {
$t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
->status_is(400)
->json_is( '/error' => "Given library_id does not exist" );
->status_is(400)->json_is( '/error' => "Given library_id does not exist" );
}
qr/DBD::mysql::st execute failed: Cannot add or update a child row: a foreign key constraint fails/;
@ -1083,8 +1049,7 @@ subtest 'update() tests' => sub {
$newpatron->{falseproperty} = "Non existent property";
$t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
->status_is(400)
->json_is( '/errors/0/message' => 'Properties not allowed: falseproperty.' );
->status_is(400)->json_is( '/errors/0/message' => 'Properties not allowed: falseproperty.' );
# Get rid of the invalid attribute
delete $newpatron->{falseproperty};
@ -1094,8 +1059,7 @@ subtest 'update() tests' => sub {
$newpatron->{userid} = $patron_1->userid;
$t->put_ok( "//$userid:$password@/api/v1/patrons/" . $patron_2->borrowernumber => json => $newpatron )
->status_is(400)
->json_has( '/error', "Problem with userid " . $patron_1->userid );
->status_is(400)->json_has( '/error', "Problem with userid " . $patron_1->userid );
$newpatron->{cardnumber} = $patron_1->id . $patron_2->id;
$newpatron->{userid} = "user" . $patron_1->id . $patron_2->id;
@ -1117,6 +1081,8 @@ subtest 'update() tests' => sub {
$newpatron->{restricted} = $unauthorized_patron->to_api( { user => $authorized_patron } )->{restricted};
$newpatron->{expired} = $unauthorized_patron->to_api( { user => $authorized_patron } )->{expired};
$newpatron->{anonymized} = $unauthorized_patron->to_api( { user => $authorized_patron } )->{anonymized};
$newpatron->{self_renewal_available} =
$unauthorized_patron->to_api( { user => $authorized_patron } )->{self_renewal_available};
my $got = $result->tx->res->json;
my $updated_on_got = delete $got->{updated_on};
@ -1152,6 +1118,7 @@ subtest 'update() tests' => sub {
delete $newpatron->{restricted};
delete $newpatron->{expired};
delete $newpatron->{anonymized};
delete $newpatron->{self_renewal_available};
# attempt to update
$authorized_patron->flags( 2**4 )->store; # borrowers flag = 4
@ -1179,7 +1146,7 @@ subtest 'update() tests' => sub {
->status_is(
403,
"Non-superlibrarian user change of superlibrarian secondary_email to undefined forbidden"
)->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
)->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
$newpatron->{secondary_email} = $superlibrarian->emailpro;
$newpatron->{altaddress_email} = 'nonsense@no.no';
@ -1195,7 +1162,7 @@ subtest 'update() tests' => sub {
->status_is(
403,
"Non-superlibrarian user change of superlibrarian altaddress_email to undefined forbidden"
)->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
)->json_is( { error => "Not enough privileges to change a superlibrarian's email" } );
# update patron without sending email
delete $newpatron->{email};
@ -1266,15 +1233,13 @@ subtest 'update() tests' => sub {
"city" => "Konstanz",
"library_id" => "MPL"
}
)
->status_is(400)
)->status_is(400)
->json_is(
'/error' => "Missing mandatory extended attribute (type=" . $attr_type_mandatory->code . ")" );
$t->put_ok( "//$userid:$password@/api/v1/patrons/"
. $superlibrarian->borrowernumber => { 'x-koha-embed' => 'extended_attributes' } => json =>
$newpatron )
->status_is(400)
$newpatron )->status_is(400)
->json_is( '/error' => 'Tried to use an invalid attribute type. type=' . $deleted_attr_code )
->json_is( '/error_code' => 'invalid_attribute_type' );
@ -1291,13 +1256,11 @@ subtest 'update() tests' => sub {
$t->put_ok( "//$userid:$password@/api/v1/patrons/"
. $superlibrarian->borrowernumber => { 'x-koha-embed' => 'extended_attributes' } => json =>
$newpatron )
->status_is(400)
$newpatron )->status_is(400)
->json_is( '/error' => 'Your action breaks a unique constraint on the attribute. type='
. $attr_type_unique->code
. ' value='
. $unique_attr->attribute )
->json_is( '/error_code' => 'attribute_not_unique' );
. $unique_attr->attribute )->json_is( '/error_code' => 'attribute_not_unique' );
$newpatron->{extended_attributes} = [
{ type => $attr_type_repeatable->code, value => 'a' },
@ -1308,13 +1271,11 @@ subtest 'update() tests' => sub {
$t->put_ok( "//$userid:$password@/api/v1/patrons/"
. $superlibrarian->borrowernumber => { 'x-koha-embed' => 'extended_attributes' } => json =>
$newpatron )
->status_is(400)
$newpatron )->status_is(400)
->json_is( '/error' => 'Your action breaks a unique constraint on the attribute. type='
. $attr_type_unique->code
. ' value='
. $unique_attr->attribute )
->json_is( '/error_code' => 'attribute_not_unique' );
. $unique_attr->attribute )->json_is( '/error_code' => 'attribute_not_unique' );
$newpatron->{extended_attributes} = [
{ type => $attr_type_repeatable->code, value => 'a' },
@ -1324,12 +1285,10 @@ subtest 'update() tests' => sub {
$t->put_ok( "//$userid:$password@/api/v1/patrons/"
. $superlibrarian->borrowernumber => { 'x-koha-embed' => 'extended_attributes' } => json =>
$newpatron )
->status_is(400)
$newpatron )->status_is(400)
->json_is( '/error' => 'Tried to add more than one non-repeatable attributes. type='
. $attr_type_mandatory->code
. ' value=pong' )
->json_is( '/error_code' => 'non_repeatable_attribute' );
. ' value=pong' )->json_is( '/error_code' => 'non_repeatable_attribute' );
my $unique_value = $unique_attr->attribute;
$unique_attr->delete;
@ -1388,8 +1347,7 @@ subtest 'delete() tests' => sub {
t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->borrowernumber );
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 409, 'Anonymous patron cannot be deleted' )
->json_is(
->status_is( 409, 'Anonymous patron cannot be deleted' )->json_is(
{
error => 'Anonymous patron cannot be deleted',
error_code => 'is_anonymous_patron'
@ -1411,8 +1369,7 @@ subtest 'delete() tests' => sub {
$guarantee->add_guarantor( { guarantor_id => $patron->id, relationship => 'parent' } );
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 409, 'Patron with checkouts cannot be deleted' )
->json_is(
->status_is( 409, 'Patron with checkouts cannot be deleted' )->json_is(
{
error => 'Pending checkouts prevent deletion',
error_code => 'has_checkouts'
@ -1423,8 +1380,7 @@ subtest 'delete() tests' => sub {
$checkout->delete;
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 409, 'Patron with debt cannot be deleted' )
->json_is(
->status_is( 409, 'Patron with debt cannot be deleted' )->json_is(
{
error => 'Pending debts prevent deletion',
error_code => 'has_debt'
@ -1435,8 +1391,7 @@ subtest 'delete() tests' => sub {
$patron->account->pay( { amount => 10, debits => [$debit] } );
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 409, 'Patron with guarantees cannot be deleted' )
->json_is(
->status_is( 409, 'Patron with guarantees cannot be deleted' )->json_is(
{
error => 'Patron is a guarantor and it prevents deletion',
error_code => 'has_guarantees'
@ -1449,8 +1404,7 @@ subtest 'delete() tests' => sub {
$patron->protected(1)->store();
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 409, 'Protected patron cannot be deleted' )
->json_is(
->status_is( 409, 'Protected patron cannot be deleted' )->json_is(
{
error => 'Protected patrons cannot be deleted',
error_code => 'is_protected'
@ -1460,8 +1414,7 @@ subtest 'delete() tests' => sub {
$patron->protected(0)->store();
$t->delete_ok( "//$userid:$password@/api/v1/patrons/" . $patron->borrowernumber )
->status_is( 204, 'REST3.2.4' )
->content_is( '', 'REST3.3.4' );
->status_is( 204, 'REST3.2.4' )->content_is( '', 'REST3.3.4' );
my $deleted_patrons = Koha::Old::Patrons->search( { borrowernumber => $patron->borrowernumber } );
is( $deleted_patrons->count, 1, 'The patron has been moved to the vault' );
@ -1493,13 +1446,11 @@ subtest 'guarantors_can_see_charges() tests' => sub {
)->status_is(401)->json_is( { error => "Authentication failure." } );
$t->put_ok( "//$userid:$password@/api/v1/public/patrons/$other_patron_id/guarantors/can_see_charges" => json =>
{ allowed => Mojo::JSON->true } )
->status_is(403)
{ allowed => Mojo::JSON->true } )->status_is(403)
->json_is( { error => "Unprivileged user cannot access another user's resources" } );
$t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_charges" => json =>
{ allowed => Mojo::JSON->true } )
->status_is(403)
{ allowed => Mojo::JSON->true } )->status_is(403)
->json_is( '/error', 'The current configuration doesn\'t allow the requested action.' );
t::lib::Mocks::mock_preference( 'AllowPatronToSetFinesVisibilityForGuarantor', 1 );
@ -1537,15 +1488,13 @@ subtest 'guarantors_can_see_checkouts() tests' => sub {
{ allowed => Mojo::JSON->true } )->status_is(401)->json_is( { error => "Authentication failure." } );
$t->put_ok( "//$userid:$password@/api/v1/public/patrons/$other_patron_id/guarantors/can_see_checkouts" => json =>
{ allowed => Mojo::JSON->true } )
->status_is(403)
{ allowed => Mojo::JSON->true } )->status_is(403)
->json_is( { error => "Unprivileged user cannot access another user's resources" } );
t::lib::Mocks::mock_preference( 'AllowPatronToSetCheckoutsVisibilityForGuarantor', 0 );
$t->put_ok( "//$userid:$password@/api/v1/public/patrons/$patron_id/guarantors/can_see_checkouts" => json =>
{ allowed => Mojo::JSON->true } )
->status_is(403)
{ allowed => Mojo::JSON->true } )->status_is(403)
->json_is( '/error', 'The current configuration doesn\'t allow the requested action.' );
t::lib::Mocks::mock_preference( 'AllowPatronToSetCheckoutsVisibilityForGuarantor', 1 );
@ -1588,8 +1537,7 @@ sub unauthorized_access_tests {
$unauthorized_patron->set_password( { password => $password, skip_validation => 1 } );
my $unauth_userid = $unauthorized_patron->userid;
$t->$verb_ok( "//$unauth_userid:$password\@$endpoint" => json => $json )
->status_is(403)
$t->$verb_ok( "//$unauth_userid:$password\@$endpoint" => json => $json )->status_is(403)
->json_has('/required_permissions');
};
}

View File

@ -0,0 +1,198 @@
#!/usr/bin/env perl
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <https://www.gnu.org/licenses>.
use Modern::Perl;
use Test::NoWarnings;
use Test::More tests => 3;
use Test::Mojo;
use t::lib::TestBuilder;
use t::lib::Mocks;
use Koha::Database;
use Koha::DateUtils qw( dt_from_string );
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new();
my $t = Test::Mojo->new('Koha::REST::V1');
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
subtest 'start()' => sub {
plan tests => 9;
$schema->storage->txn_begin;
my $category = $builder->build_object(
{
class => 'Koha::Patron::Categories',
value => {
self_renewal_enabled => 0, self_renewal_availability_start => 10, self_renewal_if_expired => 10,
self_renewal_fines_block => 10, noissuescharge => 10,
self_renewal_failure_message => 'This is a failure message'
}
}
);
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { categorycode => $category->categorycode, dateexpiry => dt_from_string(), debarred => undef }
}
);
my $password = 'thePassword123';
$patron->set_password( { password => $password, skip_validation => 1 } );
my $userid = $patron->userid;
my $borrowernumber = $patron->borrowernumber;
$t->get_ok("//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal")
->status_is( 403, 'REST3.2.2' )->json_is( { error => "You are not eligible for self-renewal" } );
$category->self_renewal_enabled(1)->store();
t::lib::Mocks::mock_preference( 'OPACPatronDetails', 1 );
$t->get_ok("//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal")
->status_is( 200, 'REST3.2.2' )->json_is(
{
self_renewal_settings => {
self_renewal_failure_message => $category->self_renewal_failure_message,
self_renewal_information_message => $category->self_renewal_information_message,
opac_patron_details => 1
}
}
);
my $new_category = $builder->build_object(
{
class => 'Koha::Patron::Categories',
value => {
self_renewal_enabled => 1, self_renewal_availability_start => 10, self_renewal_if_expired => 10,
self_renewal_fines_block => 10, noissuescharge => 10,
self_renewal_failure_message => 'This is a failure message'
}
}
);
$t->get_ok("//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal")
->status_is( 200, 'REST3.2.2' )->json_is(
{
self_renewal_settings => {
self_renewal_failure_message => $category->self_renewal_failure_message,
self_renewal_information_message => $category->self_renewal_information_message,
opac_patron_details => 1
}
}
);
$schema->storage->txn_rollback;
};
subtest 'submit()' => sub {
plan tests => 11;
$schema->storage->txn_begin;
my $category = $builder->build_object(
{
class => 'Koha::Patron::Categories',
value => {
self_renewal_enabled => 0, self_renewal_availability_start => 10, self_renewal_if_expired => 10,
self_renewal_fines_block => 10, noissuescharge => 10,
self_renewal_failure_message => 'This is a failure message'
}
}
);
my $branch = $builder->build_object( { class => 'Koha::Libraries' } );
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => {
branchcode => $branch->branchcode, categorycode => $category->categorycode,
dateexpiry => dt_from_string(), debarred => undef, lang => 'default'
}
}
);
my $password = 'thePassword123';
$patron->set_password( { password => $password, skip_validation => 1 } );
my $userid = $patron->userid;
my $borrowernumber = $patron->borrowernumber;
$t->post_ok( "//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal" => json => {} )
->status_is( 403, 'REST3.2.2' )->json_is( { error => "You are not eligible for self-renewal" } );
t::lib::Mocks::mock_preference( 'OPACPatronDetails', 1 );
t::lib::Mocks::mock_preference( 'AutoApprovePatronProfileSettings', 0 );
$category->self_renewal_enabled(1)->store();
my $date;
if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
$date =
( dt_from_string gt dt_from_string( $patron->dateexpiry ) )
? dt_from_string
: dt_from_string( $patron->dateexpiry );
} else {
$date =
C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
? dt_from_string( $patron->dateexpiry )
: dt_from_string;
}
my $expiry_date = $patron->category->get_expiry_date($date);
my $renewal_notice = $builder->build_object(
{
class => 'Koha::Notice::Templates',
value => {
module => 'members',
code => 'MEMBERSHIP_RENEWED',
branchcode => $branch->branchcode,
message_transport_type => 'print',
lang => 'default'
}
}
);
my $counter = Koha::Notice::Messages->search( { borrowernumber => $patron->borrowernumber } )->count;
$t->post_ok( "//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal" => json => {} )
->status_is( 201, 'REST3.2.2' )
->json_is( { expiry_date => $expiry_date->truncate( to => 'day' ), confirmation_sent => 1 } );
is(
Koha::Notice::Messages->search( { borrowernumber => $patron->borrowernumber } )->count, $counter + 1,
"Notice queued"
);
# Test that modifications are created correctly
t::lib::Mocks::mock_preference( 'OPACPatronDetails', 1 );
my $modification_data = { patron => { firstname => 'Newname' } };
$patron->dateexpiry( dt_from_string() )->store();
$t->post_ok(
"//$userid:$password@/api/v1/public/patrons/$borrowernumber/self_renewal" => json => $modification_data )
->status_is( 201, 'REST3.2.2' )
->json_is( { expiry_date => $expiry_date->truncate( to => 'day' ), confirmation_sent => 1 } );
my @modifications = Koha::Patron::Modifications->search( { borrowernumber => $patron->borrowernumber } )->as_list;
is( scalar(@modifications), 1, "New modification has replaced any existing mods" );
$schema->storage->txn_rollback;
};