X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=t%2Fdb_dependent%2Fapi%2Fv1%2Fholds.t;h=5abba54739677ff8a368b1d9876cd5473946a633;hb=9d6d641d1f8b77271800f43bc027b651f9aea52b;hp=535c718401d959d94fa4a043b26657c5994a9e48;hpb=d7407055a891e723c7840739ff277b699668a56d;p=srvgit diff --git a/t/db_dependent/api/v1/holds.t b/t/db_dependent/api/v1/holds.t old mode 100644 new mode 100755 index 535c718401..5abba54739 --- a/t/db_dependent/api/v1/holds.t +++ b/t/db_dependent/api/v1/holds.t @@ -17,16 +17,18 @@ use Modern::Perl; -use Test::More tests => 8; +use Test::More tests => 13; +use Test::MockModule; use Test::Mojo; use t::lib::TestBuilder; use t::lib::Mocks; use DateTime; +use Mojo::JSON qw(encode_json); use C4::Context; use Koha::Patrons; -use C4::Reserves; +use C4::Reserves qw( AddReserve CanItemBeReserved CanBookBeReserved ); use C4::Items; use Koha::Database; @@ -47,6 +49,7 @@ my $t = Test::Mojo->new('Koha::REST::V1'); my $categorycode = $builder->build({ source => 'Category' })->{categorycode}; my $branchcode = $builder->build({ source => 'Branch' })->{branchcode}; +my $branchcode2 = $builder->build({ source => 'Branch' })->{branchcode}; my $itemtype = $builder->build({ source => 'Itemtype' })->{itemtype}; # Generic password for everyone @@ -152,16 +155,15 @@ my $suspended_until = DateTime->now->add(days => 10)->truncate( to => 'day' ); my $expiration_date = DateTime->now->add(days => 10)->truncate( to => 'day' ); my $post_data = { - patron_id => int($patron_1->borrowernumber), - biblio_id => int($biblio_1->biblionumber), - item_id => int($item_1->itemnumber), + patron_id => $patron_1->borrowernumber, + biblio_id => $biblio_1->biblionumber, + item_id => $item_1->itemnumber, pickup_library_id => $branchcode, - expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }), - priority => 2, + expiration_date => output_pref( { dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 } ), }; -my $put_data = { - priority => 2, - suspended_until => output_pref({ dt => $suspended_until, dateformat => 'rfc3339' }), +my $patch_data = { + priority => 2, + suspended_until => output_pref( { dt => $suspended_until, dateformat => 'rfc3339' } ), }; subtest "Test endpoints without authentication" => sub { @@ -170,7 +172,7 @@ subtest "Test endpoints without authentication" => sub { ->status_is(401); $t->post_ok('/api/v1/holds') ->status_is(401); - $t->put_ok('/api/v1/holds/0') + $t->patch_ok('/api/v1/holds/0') ->status_is(401); $t->delete_ok('/api/v1/holds/0') ->status_is(401); @@ -189,7 +191,7 @@ subtest "Test endpoints without permission" => sub { $t->post_ok( "//$nopermission_userid:$password@/api/v1/holds" => json => $post_data ) ->status_is(403); - $t->put_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $put_data ) + $t->patch_ok( "//$nopermission_userid:$password@/api/v1/holds/0" => json => $patch_data ) ->status_is(403); $t->delete_ok( "//$nopermission_userid:$password@/api/v1/holds/0" ) @@ -211,20 +213,11 @@ subtest "Test endpoints with permission" => sub { ->json_is('/0/patron_id', $patron_2->borrowernumber) ->json_hasnt('/1'); - # While suspended_until is date-time, it's always set to midnight. - my $expected_suspended_until = $suspended_until->strftime('%FT00:00:00%z'); - substr($expected_suspended_until, -2, 0, ':'); - - $t->put_ok( "//$userid_1:$password@/api/v1/holds/$reserve_id" => json => $put_data ) - ->status_is(200) - ->json_is( '/hold_id', $reserve_id ) - ->json_is( '/suspended_until', $expected_suspended_until ) - ->json_is( '/priority', 2 ); - $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" ) - ->status_is(200); + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); - $t->put_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $put_data ) + $t->patch_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" => json => $patch_data ) ->status_is(404) ->json_has('/error'); @@ -242,11 +235,19 @@ subtest "Test endpoints with permission" => sub { ->json_is([]); $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id2" ) - ->status_is(200); + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); + + # Make sure pickup location checks doesn't get in the middle + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; }); + my $mock_item = Test::MockModule->new('Koha::Item'); + $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search }); $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) ->status_is(201) ->json_has('/hold_id'); + # Get id from response $reserve_id = $t->tx->res->json->{hold_id}; @@ -260,16 +261,30 @@ subtest "Test endpoints with permission" => sub { ->status_is(403) ->json_like('/error', qr/itemAlreadyOnHold/); - $post_data->{biblionumber} = int($biblio_2->biblionumber); - $post_data->{itemnumber} = int($item_2->itemnumber); + $post_data->{biblio_id} = $biblio_2->biblionumber; + $post_data->{item_id} = $item_2->itemnumber; $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) ->status_is(403) - ->json_like('/error', qr/itemAlreadyOnHold/); + ->json_like('/error', qr/Hold cannot be placed. Reason: tooManyReserves/); + + my $to_delete_patron = $builder->build_object({ class => 'Koha::Patrons' }); + my $deleted_patron_id = $to_delete_patron->borrowernumber; + $to_delete_patron->delete; + + my $tmp_patron_id = $post_data->{patron_id}; + $post_data->{patron_id} = $deleted_patron_id; + $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) + ->status_is(400) + ->json_is( { error => 'patron_id not found' } ); + + # Restore the original patron_id as it is expected by the next subtest + # FIXME: this tests need to be rewritten from scratch + $post_data->{patron_id} = $tmp_patron_id; }; subtest 'Reserves with itemtype' => sub { - plan tests => 9; + plan tests => 10; my $post_data = { patron_id => int($patron_1->borrowernumber), @@ -279,7 +294,14 @@ subtest 'Reserves with itemtype' => sub { }; $t->delete_ok( "//$userid_3:$password@/api/v1/holds/$reserve_id" ) - ->status_is(200); + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); + + # Make sure pickup location checks doesn't get in the middle + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; }); + my $mock_item = Test::MockModule->new('Koha::Item'); + $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search }); $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) ->status_is(201) @@ -303,13 +325,12 @@ subtest 'test AllowHoldDateInFuture' => sub { my $future_hold_date = DateTime->now->add(days => 10)->truncate( to => 'day' ); my $post_data = { - patron_id => int($patron_1->borrowernumber), - biblio_id => int($biblio_1->biblionumber), - item_id => int($item_1->itemnumber), + patron_id => $patron_1->borrowernumber, + biblio_id => $biblio_1->biblionumber, + item_id => $item_1->itemnumber, pickup_library_id => $branchcode, - expiration_date => output_pref({ dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 }), - hold_date => output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 }), - priority => 2, + expiration_date => output_pref( { dt => $expiration_date, dateformat => 'rfc3339', dateonly => 1 } ), + hold_date => output_pref( { dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 } ), }; t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 0 ); @@ -320,44 +341,120 @@ subtest 'test AllowHoldDateInFuture' => sub { t::lib::Mocks::mock_preference( 'AllowHoldDateInFuture', 1 ); + # Make sure pickup location checks doesn't get in the middle + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; }); + my $mock_item = Test::MockModule->new('Koha::Item'); + $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search }); + $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) ->status_is(201) ->json_is('/hold_date', output_pref({ dt => $future_hold_date, dateformat => 'rfc3339', dateonly => 1 })); }; -subtest 'test AllowHoldPolicyOverride' => sub { +$schema->storage->txn_rollback; - plan tests => 5; +subtest 'x-koha-override and AllowHoldPolicyOverride tests' => sub { - $dbh->do('DELETE FROM reserves'); + plan tests => 18; - Koha::CirculationRules->set_rules( + $schema->storage->txn_begin; + + my $patron = $builder->build_object( { - itemtype => undef, - branchcode => undef, - rules => { - holdallowed => 1 - } + class => 'Koha::Patrons', + value => { flags => 1 } } ); + my $password = 'thePassword123'; + $patron->set_password( { password => $password, skip_validation => 1 } ); + $patron->discard_changes; + my $userid = $patron->userid; + + my $renegade_library = $builder->build_object({ class => 'Koha::Libraries' }); t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 ); - $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) + # Make sure pickup location checks doesn't get in the middle + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + $mock_biblio->mock( 'pickup_locations', + sub { return Koha::Libraries->search({ branchcode => { '!=' => $renegade_library->branchcode } }); } ); + my $mock_item = Test::MockModule->new('Koha::Item'); + $mock_item->mock( 'pickup_locations', + sub { return Koha::Libraries->search({ branchcode => { '!=' => $renegade_library->branchcode } }) } ); + + my $can_item_be_reserved_result; + my $mock_reserves = Test::MockModule->new('C4::Reserves'); + $mock_reserves->mock( + 'CanItemBeReserved', + sub { + return $can_item_be_reserved_result; + } + ); + + my $item = $builder->build_sample_item; + + my $post_data = { + item_id => $item->id, + biblio_id => $item->biblionumber, + patron_id => $patron->id, + pickup_library_id => $patron->branchcode, + }; + + $can_item_be_reserved_result = { status => 'ageRestricted' }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) ->status_is(403) - ->json_has('/error'); + ->json_is( '/error' => "Hold cannot be placed. Reason: ageRestricted" ); + + # x-koha-override doesn't override if AllowHoldPolicyOverride not set + $t->post_ok( "//$userid:$password@/api/v1/holds" => + { 'x-koha-override' => 'any' } => json => $post_data ) + ->status_is(403); t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 ); - $t->post_ok( "//$userid_3:$password@/api/v1/holds" => json => $post_data ) + $can_item_be_reserved_result = { status => 'pickupNotInHoldGroup' }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) + ->status_is(403) + ->json_is( + '/error' => "Hold cannot be placed. Reason: pickupNotInHoldGroup" ); + + # x-koha-override overrides the status + $t->post_ok( "//$userid:$password@/api/v1/holds" => + { 'x-koha-override' => 'any' } => json => $post_data ) ->status_is(201); -}; -$schema->storage->txn_rollback; + $can_item_be_reserved_result = { status => 'OK' }; + + # x-koha-override works when status not need override + $t->post_ok( "//$userid:$password@/api/v1/holds" => + { 'x-koha-override' => 'any' } => json => $post_data ) + ->status_is(201); + + # Test pickup locations can be overridden + $post_data->{pickup_library_id} = $renegade_library->branchcode; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) + ->status_is(400); + + $t->post_ok( "//$userid:$password@/api/v1/holds" => + { 'x-koha-override' => 'any' } => json => $post_data ) + ->status_is(201); + + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0); + + $t->post_ok( "//$userid:$password@/api/v1/holds" => + { 'x-koha-override' => 'any' } => json => $post_data ) + ->status_is(400); + + $schema->storage->txn_rollback; +}; subtest 'suspend and resume tests' => sub { - plan tests => 21; + plan tests => 24; $schema->storage->txn_begin; @@ -374,7 +471,7 @@ subtest 'suspend and resume tests' => sub { my $hold = $builder->build_object( { class => 'Koha::Holds', - value => { suspend => 0, suspend_until => undef, waitingdate => undef } + value => { suspend => 0, suspend_until => undef, waitingdate => undef, found => undef } } ); @@ -385,19 +482,32 @@ subtest 'suspend and resume tests' => sub { $hold->discard_changes; # refresh object ok( $hold->is_suspended, 'Hold is suspended' ); + $t->json_is('/end_date', undef, 'Hold suspension has no end date'); + + my $end_date = output_pref({ + dt => dt_from_string( undef ), + dateformat => 'rfc3339', + dateonly => 1 + }); + + $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" => json => { end_date => $end_date } ); + + $hold->discard_changes; # refresh object + + ok( $hold->is_suspended, 'Hold is suspended' ); $t->json_is( - '/end_date', - output_pref( - { dt => dt_from_string( $hold->suspend_until ), - dateformat => 'rfc3339', - dateonly => 1 - } - ) + '/end_date', + output_pref({ + dt => dt_from_string( $hold->suspend_until ), + dateformat => 'rfc3339', + dateonly => 1 + }), + 'Hold suspension has correct end date' ); $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" ) - ->status_is( 204, "Correct status when deleting a resource" ) - ->json_is( undef ); + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); # Pass a an expiration date for the suspension my $date = dt_from_string()->add( days => 5 ); @@ -414,8 +524,8 @@ subtest 'suspend and resume tests' => sub { ->header_is( Location => "/api/v1/holds/" . $hold->id . "/suspension", 'The Location header is set' ); $t->delete_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" ) - ->status_is( 204, "Correct status when deleting a resource" ) - ->json_is( undef ); + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); $hold->set_waiting->discard_changes; @@ -423,10 +533,10 @@ subtest 'suspend and resume tests' => sub { ->status_is( 400, 'Cannot suspend waiting hold' ) ->json_is( '/error', 'Found hold cannot be suspended. Status=W' ); - $hold->set_waiting(1)->discard_changes; + $hold->set_transfer->discard_changes; $t->post_ok( "//$userid:$password@/api/v1/holds/" . $hold->id . "/suspension" ) - ->status_is( 400, 'Cannot suspend waiting hold' ) + ->status_is( 400, 'Cannot suspend hold on transfer' ) ->json_is( '/error', 'Found hold cannot be suspended. Status=T' ); $schema->storage->txn_rollback; @@ -538,3 +648,680 @@ subtest 'PUT /holds/{hold_id}/priority tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'add() tests (maxreserves behaviour)' => sub { + + plan tests => 7; + + $schema->storage->txn_begin; + + $dbh->do('DELETE FROM reserves'); + + Koha::CirculationRules->new->delete; + + my $password = 'AbcdEFG123'; + + my $patron = $builder->build_object( + { class => 'Koha::Patrons', value => { userid => 'tomasito', flags => 1 } } ); + $patron->set_password({ password => $password, skip_validation => 1 }); + my $userid = $patron->userid; + + Koha::CirculationRules->set_rules( + { + itemtype => undef, + branchcode => undef, + categorycode => undef, + rules => { + reservesallowed => 3 + } + } + ); + + Koha::CirculationRules->set_rules( + { + branchcode => undef, + categorycode => $patron->categorycode, + rules => { + max_holds => 4, + } + } + ); + + my $biblio_1 = $builder->build_sample_biblio; + my $item_1 = $builder->build_sample_item({ biblionumber => $biblio_1->biblionumber }); + my $biblio_2 = $builder->build_sample_biblio; + my $item_2 = $builder->build_sample_item({ biblionumber => $biblio_2->biblionumber }); + my $biblio_3 = $builder->build_sample_biblio; + my $item_3 = $builder->build_sample_item({ biblionumber => $biblio_3->biblionumber }); + + # Make sure pickup location checks doesn't get in the middle + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + $mock_biblio->mock( 'pickup_locations', sub { return Koha::Libraries->search; }); + my $mock_item = Test::MockModule->new('Koha::Item'); + $mock_item->mock( 'pickup_locations', sub { return Koha::Libraries->search }); + + # Disable logging + t::lib::Mocks::mock_preference( 'HoldsLog', 0 ); + t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + t::lib::Mocks::mock_preference( 'maxreserves', 2 ); + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 ); + + my $post_data = { + patron_id => $patron->borrowernumber, + biblio_id => $biblio_1->biblionumber, + pickup_library_id => $item_1->home_branch->branchcode, + item_type => $item_1->itype, + }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) + ->status_is(201); + + $post_data = { + patron_id => $patron->borrowernumber, + biblio_id => $biblio_2->biblionumber, + pickup_library_id => $item_2->home_branch->branchcode, + item_id => $item_2->itemnumber + }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) + ->status_is(201); + + $post_data = { + patron_id => $patron->borrowernumber, + biblio_id => $biblio_3->biblionumber, + pickup_library_id => $item_1->home_branch->branchcode, + item_id => $item_3->itemnumber + }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) + ->status_is(403) + ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } ); + + $schema->storage->txn_rollback; +}; + +subtest 'pickup_locations() tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 ); + + # Small trick to ease testing + Koha::Libraries->search->update({ pickup_location => 0 }); + + my $library_1 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'A', pickup_location => 1 } }); + my $library_2 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'B', pickup_location => 1 } }); + my $library_3 = $builder->build_object({ class => 'Koha::Libraries', value => { marcorgcode => 'C', pickup_location => 1 } }); + + my $library_1_api = $library_1->to_api(); + my $library_2_api = $library_2->to_api(); + my $library_3_api = $library_3->to_api(); + + $library_1_api->{needs_override} = Mojo::JSON->false; + $library_2_api->{needs_override} = Mojo::JSON->false; + $library_3_api->{needs_override} = Mojo::JSON->false; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { userid => 'tomasito', flags => 0 } + } + ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + $builder->build( + { + source => 'UserPermission', + value => { + borrowernumber => $patron->borrowernumber, + module_bit => 6, + code => 'place_holds', + }, + } + ); + + my $item_class = Test::MockModule->new('Koha::Item'); + $item_class->mock( + 'pickup_locations', + sub { + my ( $self, $params ) = @_; + my $mock_patron = $params->{patron}; + is( $mock_patron->borrowernumber, + $patron->borrowernumber, 'Patron passed correctly' ); + return Koha::Libraries->search( + { + branchcode => { + '-in' => [ + $library_1->branchcode, + $library_2->branchcode + ] + } + }, + { # we make sure no surprises in the order of the result + order_by => { '-asc' => 'marcorgcode' } + } + ); + } + ); + + my $biblio_class = Test::MockModule->new('Koha::Biblio'); + $biblio_class->mock( + 'pickup_locations', + sub { + my ( $self, $params ) = @_; + my $mock_patron = $params->{patron}; + is( $mock_patron->borrowernumber, + $patron->borrowernumber, 'Patron passed correctly' ); + return Koha::Libraries->search( + { + branchcode => { + '-in' => [ + $library_2->branchcode, + $library_3->branchcode + ] + } + }, + { # we make sure no surprises in the order of the result + order_by => { '-asc' => 'marcorgcode' } + } + ); + } + ); + + my $item = $builder->build_sample_item; + + # biblio-level hold + my $hold_1 = $builder->build_object( + { + class => 'Koha::Holds', + value => { + itemnumber => undef, + biblionumber => $item->biblionumber, + borrowernumber => $patron->borrowernumber + } + } + ); + # item-level hold + my $hold_2 = $builder->build_object( + { + class => 'Koha::Holds', + value => { + itemnumber => $item->itemnumber, + biblionumber => $item->biblionumber, + borrowernumber => $patron->borrowernumber + } + } + ); + + $t->get_ok( "//$userid:$password@/api/v1/holds/" + . $hold_1->id + . "/pickup_locations" ) + ->json_is( [ $library_2_api, $library_3_api ] ); + + $t->get_ok( "//$userid:$password@/api/v1/holds/" + . $hold_2->id + . "/pickup_locations" ) + ->json_is( [ $library_1_api, $library_2_api ] ); + + # filtering works! + $t->get_ok( "//$userid:$password@/api/v1/holds/" + . $hold_2->id + . '/pickup_locations?q={"marc_org_code": { "-like": "A%" }}' ) + ->json_is( [ $library_1_api ] ); + + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 ); + + my $library_4 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 0, marcorgcode => 'X' } }); + my $library_5 = $builder->build_object({ class => 'Koha::Libraries', value => { pickup_location => 1, marcorgcode => 'Y' } }); + + my $library_5_api = $library_5->to_api(); + $library_5_api->{needs_override} = Mojo::JSON->true; + + # bibli-level mock doesn't include library_1 as valid pickup location + $library_1_api->{needs_override} = Mojo::JSON->true; + + $t->get_ok( "//$userid:$password@/api/v1/holds/" + . $hold_1->id + . "/pickup_locations?_order_by=marc_org_code" ) + ->json_is( [ $library_1_api, $library_2_api, $library_3_api, $library_5_api ] ); + + $schema->storage->txn_rollback; +}; + +subtest 'edit() tests' => sub { + + plan tests => 37; + + $schema->storage->txn_begin; + + my $password = 'AbcdEFG123'; + + my $library = $builder->build_object({ class => 'Koha::Libraries' }); + my $patron = $builder->build_object( + { class => 'Koha::Patrons', value => { flags => 1 } } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + $builder->build( + { + source => 'UserPermission', + value => { + borrowernumber => $patron->borrowernumber, + module_bit => 6, + code => 'modify_holds_priority', + }, + } + ); + + # Disable logging + t::lib::Mocks::mock_preference( 'HoldsLog', 0 ); + t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 ); + + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + my $mock_item = Test::MockModule->new('Koha::Item'); + + my $library_1 = $builder->build_object({ class => 'Koha::Libraries' }); + my $library_2 = $builder->build_object({ class => 'Koha::Libraries' }); + my $library_3 = $builder->build_object({ class => 'Koha::Libraries' }); + + # let's control what Koha::Biblio->pickup_locations returns, for testing + $mock_biblio->mock( 'pickup_locations', sub { + return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } ); + }); + # let's mock what Koha::Item->pickup_locations returns, for testing + $mock_item->mock( 'pickup_locations', sub { + return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } ); + }); + + my $biblio = $builder->build_sample_biblio; + my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber }); + + # Test biblio-level holds + my $biblio_hold = $builder->build_object( + { + class => "Koha::Holds", + value => { + biblionumber => $biblio->biblionumber, + branchcode => $library_3->branchcode, + itemnumber => undef, + priority => 1, + } + } + ); + + my $biblio_hold_api_data = $biblio_hold->to_api; + my $biblio_hold_data = { + pickup_library_id => $library_1->branchcode, + priority => $biblio_hold_api_data->{priority} + }; + + $t->patch_ok( "//$userid:$password@/api/v1/holds/" + . $biblio_hold->id + => json => $biblio_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $biblio_hold->id + => json => $biblio_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $biblio_hold->discard_changes; + is( $biblio_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' ); + + $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id + => { 'x-koha-override' => 'any' } + => json => $biblio_hold_data ) + ->status_is(200) + ->json_has( '/pickup_library_id' => $library_1->id ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" . $biblio_hold->id + => { 'x-koha-override' => 'any' } + => json => $biblio_hold_data ) + ->status_is(200) + ->json_has( '/pickup_library_id' => $library_1->id ); + + $biblio_hold_data->{pickup_library_id} = $library_2->branchcode; + $t->patch_ok( "//$userid:$password@/api/v1/holds/" + . $biblio_hold->id + => json => $biblio_hold_data ) + ->status_is(200); + + $biblio_hold->discard_changes; + is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $biblio_hold->id + => json => $biblio_hold_data ) + ->status_is(200); + + $biblio_hold->discard_changes; + is( $biblio_hold->branchcode, $library_2->id, 'Pickup location changed correctly' ); + + # Test item-level holds + my $item_hold = $builder->build_object( + { + class => "Koha::Holds", + value => { + biblionumber => $biblio->biblionumber, + branchcode => $library_3->branchcode, + itemnumber => $item->itemnumber, + priority => 1, + } + } + ); + + my $item_hold_api_data = $item_hold->to_api; + my $item_hold_data = { + pickup_library_id => $library_1->branchcode, + priority => $item_hold_api_data->{priority} + }; + + $t->patch_ok( "//$userid:$password@/api/v1/holds/" + . $item_hold->id + => json => $item_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $item_hold->id + => json => $item_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $item_hold->discard_changes; + is( $item_hold->branchcode, $library_3->branchcode, 'branchcode remains untouched' ); + + $t->patch_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id + => { 'x-koha-override' => 'any' } + => json => $item_hold_data ) + ->status_is(200) + ->json_has( '/pickup_library_id' => $library_1->id ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" . $item_hold->id + => { 'x-koha-override' => 'any' } + => json => $item_hold_data ) + ->status_is(200) + ->json_has( '/pickup_library_id' => $library_1->id ); + + $item_hold_data->{pickup_library_id} = $library_2->branchcode; + $t->patch_ok( "//$userid:$password@/api/v1/holds/" + . $item_hold->id + => json => $item_hold_data ) + ->status_is(200); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $item_hold->id + => json => $item_hold_data ) + ->status_is(200); + + $item_hold->discard_changes; + is( $item_hold->branchcode, $library_2->id, 'Pickup location changed correctly' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 10; + + $schema->storage->txn_begin; + + my $password = 'AbcdEFG123'; + + my $library = $builder->build_object({ class => 'Koha::Libraries' }); + my $patron = $builder->build_object( + { class => 'Koha::Patrons', value => { flags => 1 } } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + $builder->build( + { + source => 'UserPermission', + value => { + borrowernumber => $patron->borrowernumber, + module_bit => 6, + code => 'modify_holds_priority', + }, + } + ); + + # Disable logging + t::lib::Mocks::mock_preference( 'HoldsLog', 0 ); + t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + + my $mock_biblio = Test::MockModule->new('Koha::Biblio'); + my $mock_item = Test::MockModule->new('Koha::Item'); + + my $library_1 = $builder->build_object({ class => 'Koha::Libraries' }); + my $library_2 = $builder->build_object({ class => 'Koha::Libraries' }); + my $library_3 = $builder->build_object({ class => 'Koha::Libraries' }); + + # let's control what Koha::Biblio->pickup_locations returns, for testing + $mock_biblio->mock( 'pickup_locations', sub { + return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } ); + }); + # let's mock what Koha::Item->pickup_locations returns, for testing + $mock_item->mock( 'pickup_locations', sub { + return Koha::Libraries->search( { branchcode => [ $library_2->branchcode, $library_3->branchcode ] } ); + }); + + my $can_be_reserved = 'OK'; + my $mock_reserves = Test::MockModule->new('C4::Reserves'); + $mock_reserves->mock( 'CanItemBeReserved', sub + { + return { status => $can_be_reserved } + } + + ); + $mock_reserves->mock( 'CanBookBeReserved', sub + { + return { status => $can_be_reserved } + } + + ); + + my $biblio = $builder->build_sample_biblio; + my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber }); + + # Test biblio-level holds + my $biblio_hold = $builder->build_object( + { + class => "Koha::Holds", + value => { + biblionumber => $biblio->biblionumber, + branchcode => $library_3->branchcode, + itemnumber => undef, + priority => 1, + } + } + ); + + my $biblio_hold_api_data = $biblio_hold->to_api; + $biblio_hold->delete; + my $biblio_hold_data = { + biblio_id => $biblio_hold_api_data->{biblio_id}, + patron_id => $biblio_hold_api_data->{patron_id}, + pickup_library_id => $library_1->branchcode, + }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $biblio_hold_data->{pickup_library_id} = $library_2->branchcode; + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $biblio_hold_data ) + ->status_is(201); + + # Test item-level holds + my $item_hold = $builder->build_object( + { + class => "Koha::Holds", + value => { + biblionumber => $biblio->biblionumber, + branchcode => $library_3->branchcode, + itemnumber => $item->itemnumber, + priority => 1, + } + } + ); + + my $item_hold_api_data = $item_hold->to_api; + $item_hold->delete; + my $item_hold_data = { + biblio_id => $item_hold_api_data->{biblio_id}, + item_id => $item_hold_api_data->{item_id}, + patron_id => $item_hold_api_data->{patron_id}, + pickup_library_id => $library_1->branchcode, + }; + + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data ) + ->status_is(400) + ->json_is({ error => 'The supplied pickup location is not valid' }); + + $item_hold_data->{pickup_library_id} = $library_2->branchcode; + $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $item_hold_data ) + ->status_is(201); + + $schema->storage->txn_rollback; +}; + +subtest 'PUT /holds/{hold_id}/pickup_location tests' => sub { + + plan tests => 28; + + $schema->storage->txn_begin; + + my $password = 'AbcdEFG123'; + + # library_1 and library_2 are available pickup locations, not library_3 + my $library_1 = $builder->build_object( + { class => 'Koha::Libraries', value => { pickup_location => 1 } } ); + my $library_2 = $builder->build_object( + { class => 'Koha::Libraries', value => { pickup_location => 1 } } ); + my $library_3 = $builder->build_object( + { class => 'Koha::Libraries', value => { pickup_location => 0 } } ); + + my $patron = $builder->build_object( + { class => 'Koha::Patrons', value => { flags => 0 } } ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $patron->userid; + $builder->build( + { + source => 'UserPermission', + value => { + borrowernumber => $patron->borrowernumber, + module_bit => 6, + code => 'place_holds', + }, + } + ); + + # Disable logging + t::lib::Mocks::mock_preference( 'HoldsLog', 0 ); + t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + + my $biblio = $builder->build_sample_biblio; + my $item = $builder->build_sample_item( + { + biblionumber => $biblio->biblionumber, + library => $library_1->branchcode + } + ); + + # biblio-level hold + my $hold = Koha::Holds->find( + AddReserve( + { + branchcode => $library_1->branchcode, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + priority => 1, + itemnumber => undef, + } + ) + ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } ) + ->status_is(200) + ->json_is({ pickup_library_id => $library_2->branchcode }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } ) + ->status_is(400) + ->json_is({ error => '[The supplied pickup location is not valid]' }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library unchanged' ); + + # item-level hold + $hold = Koha::Holds->find( + AddReserve( + { + branchcode => $library_1->branchcode, + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + priority => 1, + itemnumber => $item->itemnumber, + } + ) + ); + + # Attempt to use an invalid pickup locations ends in 400 + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } ) + ->status_is(400) + ->json_is({ error => '[The supplied pickup location is not valid]' }); + + is( $hold->discard_changes->branchcode->branchcode, $library_1->branchcode, 'pickup library unchanged' ); + + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 1 ); + + # Attempt to use an invalid pickup locations with override succeeds + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" + => { 'x-koha-override' => 'any' } + => json => { pickup_library_id => $library_2->branchcode } ) + ->status_is(200) + ->json_is({ pickup_library_id => $library_2->branchcode }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library changed' ); + + t::lib::Mocks::mock_preference( 'AllowHoldPolicyOverride', 0 ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" => json => { pickup_library_id => $library_2->branchcode } ) + ->status_is(200) + ->json_is({ pickup_library_id => $library_2->branchcode }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'pickup library adjusted correctly' ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" => json => { pickup_library_id => $library_3->branchcode } ) + ->status_is(400) + ->json_is({ error => '[The supplied pickup location is not valid]' }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used' ); + + $t->put_ok( "//$userid:$password@/api/v1/holds/" + . $hold->id + . "/pickup_location" + => { 'x-koha-override' => 'any' } + => json => { pickup_library_id => $library_3->branchcode } ) + ->status_is(400) + ->json_is({ error => '[The supplied pickup location is not valid]' }); + + is( $hold->discard_changes->branchcode->branchcode, $library_2->branchcode, 'invalid pickup library not used, even if x-koha-override is passed' ); + + $schema->storage->txn_rollback; +};