From 330957aedebc63cb5640c5160f49b2e97da1ad47 Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Thu, 19 Feb 2026 13:34:12 +0000 Subject: [PATCH] Bug 26355: Add unit tests --- Koha/REST/V1/Patrons/SelfRenewal.pm | 1 + t/db_dependent/Koha/Patron.t | 146 +++++++++++++- t/db_dependent/api/v1/patron_categories.t | 82 ++++++-- t/db_dependent/api/v1/patrons.t | 180 ++++++----------- t/db_dependent/api/v1/patrons_self_renewal.t | 198 +++++++++++++++++++ 5 files changed, 477 insertions(+), 130 deletions(-) create mode 100755 t/db_dependent/api/v1/patrons_self_renewal.t diff --git a/Koha/REST/V1/Patrons/SelfRenewal.pm b/Koha/REST/V1/Patrons/SelfRenewal.pm index 00f3a66cf48..646f0be9ce8 100644 --- a/Koha/REST/V1/Patrons/SelfRenewal.pm +++ b/Koha/REST/V1/Patrons/SelfRenewal.pm @@ -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 diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t index 44802ce4ba1..7b7774ac016 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 46; +use Test::More tests => 49; use Test::NoWarnings; use Test::Exception; use Test::Warn; @@ -721,7 +721,7 @@ subtest 'messaging_preferences() tests' => sub { subtest 'to_api() tests' => sub { - plan tests => 6; + plan tests => 7; $schema->storage->txn_begin; @@ -759,6 +759,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; }; @@ -3562,3 +3565,142 @@ subtest "create_hold_group, hold_groups, visual_hold_group_id tests" => 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; +}; diff --git a/t/db_dependent/api/v1/patron_categories.t b/t/db_dependent/api/v1/patron_categories.t index d383be3a47f..36213024db9 100755 --- a/t/db_dependent/api/v1/patron_categories.t +++ b/t/db_dependent/api/v1/patron_categories.t @@ -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,80 @@ 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; +}; + diff --git a/t/db_dependent/api/v1/patrons.t b/t/db_dependent/api/v1/patrons.t index 1ec56c94595..516faa66dc8 100755 --- a/t/db_dependent/api/v1/patrons.t +++ b/t/db_dependent/api/v1/patrons.t @@ -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; @@ -481,14 +458,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 @@ -502,8 +479,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; @@ -512,8 +488,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'/; @@ -528,8 +503,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"; @@ -546,6 +520,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 @@ -559,15 +534,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)' ); @@ -694,8 +665,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' ); @@ -710,8 +680,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" ); @@ -872,8 +841,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 @@ -931,9 +899,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 @@ -947,8 +915,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; @@ -965,8 +932,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/; @@ -977,8 +943,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}; @@ -988,8 +953,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; @@ -1011,6 +975,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}; @@ -1046,6 +1012,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 @@ -1073,7 +1040,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'; @@ -1089,7 +1056,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}; @@ -1160,15 +1127,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' ); @@ -1185,13 +1150,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' }, @@ -1202,13 +1165,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' }, @@ -1218,12 +1179,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; @@ -1282,8 +1241,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' @@ -1305,8 +1263,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' @@ -1317,8 +1274,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' @@ -1329,8 +1285,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' @@ -1343,8 +1298,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' @@ -1354,8 +1308,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' ); @@ -1387,13 +1340,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 ); @@ -1431,15 +1382,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 ); @@ -1482,8 +1431,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'); }; } diff --git a/t/db_dependent/api/v1/patrons_self_renewal.t b/t/db_dependent/api/v1/patrons_self_renewal.t new file mode 100755 index 00000000000..d9f9f8da4cc --- /dev/null +++ b/t/db_dependent/api/v1/patrons_self_renewal.t @@ -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 . + +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; +}; -- 2.50.1