From 6d884454de4cf227f820f71790306984dfb203c9 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Sun, 6 Aug 2017 23:02:16 +0000 Subject: [PATCH] Bug 19037: Issuing rules test tool - REST API endpoint for effective issuing rule To test: 1. prove t/db_dependent/api/v1/issuingrules.t 2. Send GET requests to /api/v1/issuingrules/effective with different parameters as defined in the path's Swagger spec 3. Make sure it returns the correct issuing rules (don't forget that system prefs like CircControl and HomeOrHoldingBranch affects the result) --- Koha/Exceptions/Item.pm | 18 ++ Koha/Exceptions/ItemType.pm | 18 ++ Koha/Exceptions/Library.pm | 18 ++ Koha/Exceptions/Patron.pm | 18 ++ Koha/Exceptions/Patron/Category.pm | 18 ++ Koha/REST/V1/IssuingRule.pm | 237 +++++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/issuingrule.json | 144 +++++++++++ api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/issuingrules.json | 98 +++++++ t/db_dependent/api/v1/cities.t | 2 +- t/db_dependent/api/v1/issuingrules.t | 385 ++++++++++++++++++++++++++++ 12 files changed, 961 insertions(+), 1 deletion(-) create mode 100644 Koha/Exceptions/Item.pm create mode 100644 Koha/Exceptions/ItemType.pm create mode 100644 Koha/Exceptions/Library.pm create mode 100644 Koha/Exceptions/Patron.pm create mode 100644 Koha/Exceptions/Patron/Category.pm create mode 100644 Koha/REST/V1/IssuingRule.pm create mode 100644 api/v1/swagger/definitions/issuingrule.json create mode 100644 api/v1/swagger/paths/issuingrules.json create mode 100644 t/db_dependent/api/v1/issuingrules.t diff --git a/Koha/Exceptions/Item.pm b/Koha/Exceptions/Item.pm new file mode 100644 index 0000000..abbb8ed --- /dev/null +++ b/Koha/Exceptions/Item.pm @@ -0,0 +1,18 @@ +package Koha::Exceptions::Item; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::Item' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::Item::NotFound' => { + isa => 'Koha::Exceptions::Item', + description => 'Item not found', + fields => ['itemnumber'] + }, + +); + +1; diff --git a/Koha/Exceptions/ItemType.pm b/Koha/Exceptions/ItemType.pm new file mode 100644 index 0000000..b0d263d --- /dev/null +++ b/Koha/Exceptions/ItemType.pm @@ -0,0 +1,18 @@ +package Koha::Exceptions::ItemType; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::ItemType' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::ItemType::NotFound' => { + isa => 'Koha::Exceptions::ItemType', + description => 'ItemType not found', + fields => ['itemtype'] + }, + +); + +1; diff --git a/Koha/Exceptions/Library.pm b/Koha/Exceptions/Library.pm new file mode 100644 index 0000000..f6615e6 --- /dev/null +++ b/Koha/Exceptions/Library.pm @@ -0,0 +1,18 @@ +package Koha::Exceptions::Library; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::Library' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::Library::NotFound' => { + isa => 'Koha::Exceptions::Library', + description => 'Library not found', + fields => ['branchcode'] + }, + +); + +1; diff --git a/Koha/Exceptions/Patron.pm b/Koha/Exceptions/Patron.pm new file mode 100644 index 0000000..face29c --- /dev/null +++ b/Koha/Exceptions/Patron.pm @@ -0,0 +1,18 @@ +package Koha::Exceptions::Patron; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::Patron' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::Patron::NotFound' => { + isa => 'Koha::Exceptions::Patron', + description => 'Patron not found', + fields => ['borrowernumber'] + }, + +); + +1; diff --git a/Koha/Exceptions/Patron/Category.pm b/Koha/Exceptions/Patron/Category.pm new file mode 100644 index 0000000..e81b011 --- /dev/null +++ b/Koha/Exceptions/Patron/Category.pm @@ -0,0 +1,18 @@ +package Koha::Exceptions::Patron::Category; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::Patron::Category' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::Patron::Category::NotFound' => { + isa => 'Koha::Exceptions::Patron::Category', + description => 'Patron::Category not found', + fields => ['categorycode'] + }, + +); + +1; diff --git a/Koha/REST/V1/IssuingRule.pm b/Koha/REST/V1/IssuingRule.pm new file mode 100644 index 0000000..356eac1 --- /dev/null +++ b/Koha/REST/V1/IssuingRule.pm @@ -0,0 +1,237 @@ +package Koha::REST::V1::IssuingRule; + +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use C4::Circulation; + +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; +use Koha::Libraries; +use Koha::Patron::Categories; +use Koha::Patrons; + +use Koha::Exceptions; +use Koha::Exceptions::ItemType; +use Koha::Exceptions::Library; +use Koha::Exceptions::Patron; +use Koha::Exceptions::Patron::Category; + +use Try::Tiny; + +sub get_effective { + my $c = shift->openapi->valid_input or return; + + return try { + my $params = $c->req->query_params->to_hash; + + my ($categorycode, $itemtype, $branchcode); + my $user = $c->stash('koha.user'); + my $patron = _find_patron($params); + my $item = _find_item($params); + $categorycode = _find_categorycode($params, $patron); + $itemtype = _find_itemtype($params, $item); + $branchcode = _find_branchcode($params, $item, $patron, $user); + + my $rule = Koha::IssuingRules->get_effective_issuing_rule({ + categorycode => $categorycode, + itemtype => $itemtype, + branchcode => $branchcode, + }); + + return $c->render(status => 200, openapi => $rule); + } + catch { + if ($_->isa('Koha::Exceptions::BadParameter')) { + return $c->render(status => 400, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::Item::NotFound')) { + return $c->render(status => 404, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::ItemType::NotFound')) { + return $c->render(status => 404, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::Library::NotFound')) { + return $c->render(status => 404, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) { + return $c->render(status => 404, openapi => { error => $_->error }); + } + elsif ($_->isa('Koha::Exceptions::Patron::Category::NotFound')) { + return $c->render(status => 404, openapi => { error => $_->error }); + } + else { + return $c->render(status => 500, openapi => { + error => 'Something went wrong, check the logs.' + }); + } + }; +} + +sub _find_branchcode { + my ($params, $item, $patron, $loggedin_user) = @_; + + my $circcontrol = C4::Context->preference('CircControl'); + my $branchcode; + if ($circcontrol eq 'PatronLibrary' && defined $patron + && !length $params->{branchcode}) { + $branchcode = C4::Circulation::_GetCircControlBranch( + undef, + $patron->unblessed + ); + } elsif ($circcontrol eq 'ItemHomeLibrary' && defined $item + && !length $params->{branchcode}) { + $branchcode = C4::Circulation::_GetCircControlBranch( + $item->unblessed, + undef + ); + } elsif ($circcontrol eq 'PickupLibrary' && !exists $params->{branchcode}) { + # If CircControl == PickupLibrary, expect currently logged in branch + # to be the homebranch of logged in user ONLY IF parameter branchcode + # is not provided - if the parameter exists but is not defined, allow + # the possibility to query with null branchcode + $branchcode = $loggedin_user->branchcode; + } + + return $branchcode if $branchcode; + + if (length $params->{branchcode}) { + my $library = Koha::Libraries->find($params->{branchcode}); + unless ($library) { + Koha::Exceptions::Library::NotFound->throw( + error => 'Branchcode not found' + ); + } + $branchcode = $library->branchcode; + } + + return $branchcode; + +} + +sub _find_categorycode { + my ($params, $patron) = @_; + + my $categorycode; + if (defined $patron && length $params->{categorycode}) { + unless ($patron->categorycode eq $params->{categorycode}) { + Koha::Exceptions::BadParameter->throw( + error => "Patron's categorycode does not match given categorycode" + ); + } + } + + return $patron->categorycode if $patron; + + if (length $params->{categorycode}) { + my $category = Koha::Patron::Categories->find($params->{categorycode}); + unless ($category) { + Koha::Exceptions::Patron::Category::NotFound->throw( + error => 'Categorycode not found' + ); + } + $categorycode = $category->categorycode; + } + + return $categorycode; +} + +sub _find_item { + my ($params) = @_; + + my $item; + if (length $params->{itemnumber} && length $params->{barcode}) { + $item = Koha::Items->find({ + itemnumber => $params->{itemnumber}, + barcode => $params->{barcode}, + }); + } elsif (length $params->{itemnumber}) { + $item = Koha::Items->find($params->{itemnumber}); + } elsif (length $params->{barcode}) { + $item = Koha::Items->find({ + barcode => $params->{barcode} + }); + } + + if ((length $params->{itemnumber} || length $params->{barcode}) + && !defined $item) { + Koha::Exceptions::Item::NotFound->throw( + error => 'Item not found' + ); + } + + return $item; +} + +sub _find_itemtype { + my ($params, $item) = @_; + + my $itemtype; + if (defined $item && length $params->{itemtype}) { + unless ($item->effective_itemtype eq $params->{itemtype}) { + Koha::Exceptions::BadParameter->throw( + error => "Item's item type does not match given item type" + ); + } + } + + return $item->effective_itemtype if $item; + + if (length $params->{itemtype}) { + my $itemtype_o = Koha::ItemTypes->find($params->{itemtype}); + unless ($itemtype_o) { + Koha::Exceptions::ItemType::NotFound->throw( + error => 'Item type not found' + ); + } + $itemtype = $itemtype_o->itemtype; + } + + return $itemtype; +} + +sub _find_patron { + my ($params) = @_; + + my $patron; + if (length $params->{borrowernumber} && length $params->{cardnumber}) { + $patron = Koha::Patrons->find({ + borrowernumber => $params->{borrowernumber}, + cardnumber => $params->{cardnumber}, + }); + } elsif (length $params->{borrowernumber}) { + $patron = Koha::Patrons->find($params->{borrowernumber}); + } elsif (length $params->{cardnumber}) { + $patron = Koha::Patrons->find({ + cardnumber => $params->{cardnumber} + }); + } + + if ((length $params->{borrowernumber} || length $params->{cardnumber}) + && !defined $patron) { + Koha::Exceptions::Patron::NotFound->throw( + error => 'Patron not found' + ); + } + + return $patron; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 939baa8..dfaa01d 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -11,6 +11,9 @@ "holds": { "$ref": "definitions/holds.json" }, + "issuingrule": { + "$ref": "definitions/issuingrule.json" + }, "patron": { "$ref": "definitions/patron.json" } diff --git a/api/v1/swagger/definitions/issuingrule.json b/api/v1/swagger/definitions/issuingrule.json new file mode 100644 index 0000000..4bb260b --- /dev/null +++ b/api/v1/swagger/definitions/issuingrule.json @@ -0,0 +1,144 @@ +{ + "type": "object", + "properties": { + "issuingrules_id": { + "description": "internal issuing rule id", + "type": "integer" + }, + "categorycode": { + "description": "patron category this rule is for (categories.categorycode)", + "type": "string" + }, + "itemtype": { + "description": "item type this rule is for (itemtypes.itemtype)", + "type": "string" + }, + "restrictedtype": { + "description": "not used? always NULL", + "type": ["integer", "null"] + }, + "rentaldiscount": { + "description": "percent discount on the rental charge for this item", + "type": ["number", "null"] + }, + "reservecharge": { + "description": "", + "type": ["number", "null"] + }, + "fine": { + "description": "fine amount", + "type": ["number", "null"] + }, + "finedays": { + "description": "suspension in days", + "type": ["integer", "null"] + }, + "maxsuspensiondays": { + "description": "max suspension days", + "type": ["integer", "null"] + }, + "firstremind": { + "description": "fine grace period", + "type": ["integer", "null"] + }, + "chargeperiod": { + "description": "how often the fine amount is charged", + "type": ["integer", "null"] + }, + "chargeperiod_charge_at": { + "description": "Should fine be given at the start ( 1 ) or the end ( 0 ) of the period", + "type": "integer" + }, + "accountsent": { + "description": "not used? always NULL", + "type": ["integer", "null"] + }, + "chargename": { + "description": "not used? always NULL", + "type": ["string", "null"] + }, + "maxissueqty": { + "description": "total number of checkouts allowed", + "type": ["integer", "null"] + }, + "maxonsiteissueqty": { + "description": "total number of on-site checkouts allowed", + "type": ["integer", "null"] + }, + "issuelength": { + "description": "length of checkout in the unit set in issuingrules.lengthunit", + "type": ["integer", "null"] + }, + "lengthunit": { + "description": "unit of checkout length (days, hours)", + "type": ["string", "null"] + }, + "hardduedate": { + "description": "hard due date", + "type": ["string", "null"], + "format": "date" + }, + "hardduedatecompare": { + "description": "type of hard due date (1 = after, 0 = on, -1 = before)", + "type": "integer" + }, + "renewalsallowed": { + "description": "how many renewals are allowed", + "type": "integer" + }, + "renewalperiod": { + "description": "renewal period in the unit set in issuingrules.lengthunit", + "type": ["integer", "null"] + }, + "norenewalbefore": { + "description": "no renewal allowed until X days or hours before due date.", + "type": ["integer", "null"] + }, + "auto_renew": { + "description": "automatic renewal", + "type": ["integer", "null"] + }, + "no_auto_renewal_after": { + "description": "no auto renewal allowed after X days or hours after the issue date", + "type": ["integer", "null"] + }, + "no_auto_renewal_after_hard_limit": { + "description": "no auto renewal allowed after a given date", + "type": ["string", "null"], + "format": "date" + }, + "reservesallowed": { + "description": "how many holds are allowed", + "type": "integer" + }, + "holds_per_record": { + "description": "How many holds a patron can have on a given bib", + "type": "integer" + }, + "branchcode": { + "description": "the branch this rule is for (branches.branchcode)", + "type": "string" + }, + "overduefinescap": { + "description": "the maximum amount of an overdue fine", + "type": ["number", "null"] + }, + "cap_fine_to_replacement_price": { + "description": "cap the fine based on item's replacement price", + "type": "integer" + }, + "onshelfholds": { + "description": "allow holds for items that are on shelf", + "type": "integer" + }, + "opacitemholds": { + "description": "allow opac users to place specific items on hold", + "type": "string" + }, + "article_requests": { + "description": "allow article requests to be placed,", + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index b1fcf40..e5362b6 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -11,6 +11,9 @@ "/holds/{reserve_id}": { "$ref": "paths/holds.json#/~1holds~1{reserve_id}" }, + "/issuingrules/effective": { + "$ref": "paths/issuingrules.json#/~1issuingrules~1effective" + }, "/patrons": { "$ref": "paths/patrons.json#/~1patrons" }, diff --git a/api/v1/swagger/paths/issuingrules.json b/api/v1/swagger/paths/issuingrules.json new file mode 100644 index 0000000..c1c1348 --- /dev/null +++ b/api/v1/swagger/paths/issuingrules.json @@ -0,0 +1,98 @@ +{ + "/issuingrules/effective": { + "get": { + "x-mojo-to": "IssuingRule#get_effective", + "operationId": "getEffectiveIssuingRule", + "tags": ["issuingrules"], + "parameters": [{ + "name": "branchcode", + "in": "query", + "description": "Target branchcode (internal library identifier). If parameter not given, assume logged in user's homebranch. Give empty value (e.g. issuingrules/effective?branchcode=&categorycode=ASD) to query issuing rules targetting all libraries.", + "required": false, + "type": "string" + }, { + "name": "itemtype", + "in": "query", + "description": "Target itemtype (internal item type identifier)", + "required": false, + "type": "string" + }, { + "name": "categorycode", + "in": "query", + "description": "Target patron categorycode (internal category identifier)", + "required": false, + "type": "string" + }, { + "name": "barcode", + "in": "query", + "description": "Target item (barcode)", + "required": false, + "type": "string" + }, { + "name": "itemnumber", + "in": "query", + "description": "Target item (internal item identifier)", + "required": false, + "type": "integer" + }, { + "name": "borrowernumber", + "in": "query", + "description": "Target patron (internal patron identifier)", + "required": false, + "type": "integer" + }, { + "name": "cardnumber", + "in": "query", + "description": "Target patron card number", + "required": false, + "type": "string" + }], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Effective issuing rule", + "schema": { "$ref": "../definitions.json#/issuingrule" } + }, + "400": { + "description": "Bad parameter", + "schema": { "$ref": "../definitions.json#/error" } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "One or more of the given parameters not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "parameters": "manage_circ_rules" + } + } + } + } +} diff --git a/t/db_dependent/api/v1/cities.t b/t/db_dependent/api/v1/cities.t index ac8fe43..5652f36 100644 --- a/t/db_dependent/api/v1/cities.t +++ b/t/db_dependent/api/v1/cities.t @@ -33,7 +33,7 @@ my $builder = t::lib::TestBuilder->new; # FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling # this affects the other REST api tests -t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); +#t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); my $remote_address = '127.0.0.1'; my $t = Test::Mojo->new('Koha::REST::V1'); diff --git a/t/db_dependent/api/v1/issuingrules.t b/t/db_dependent/api/v1/issuingrules.t new file mode 100644 index 0000000..bd31576 --- /dev/null +++ b/t/db_dependent/api/v1/issuingrules.t @@ -0,0 +1,385 @@ +#!/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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Test::More tests => 1; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; + +use Koha::Database; +use Koha::IssuingRules; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling +# this affects the other REST api tests +t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); + +my $remote_address = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +subtest 'get_effective() tests' => sub { + + plan tests => 27; + + $schema->storage->txn_begin; + + # Empty current issuign rules and create two random issuing rules + Koha::IssuingRules->search->delete; + + my $context1 = create_test_context(); + my $context2 = create_test_context(); + my $context3 = create_test_context(); + + create_issuing_rule({ + categorycode => '*', + itemtype => '*', + branchcode => '*' + }); + create_issuing_rule(); + create_issuing_rule(); + create_issuing_rule({ + categorycode => $context1->{patron}->{categorycode}, + itemtype => $context1->{item}->{itype}, + branchcode => '*' + }); + create_issuing_rule({ + categorycode => $context2->{patron}->{categorycode}, + itemtype => $context2->{item}->{itype}, + branchcode => $context2->{patron}->{branchcode} + }); + create_issuing_rule({ + categorycode => $context3->{patron}->{categorycode}, + itemtype => $context3->{item}->{itype}, + branchcode => $context3->{item}->{homebranch} + }); + create_issuing_rule({ + categorycode => $context3->{patron}->{categorycode}, + itemtype => $context3->{item}->{itype}, + branchcode => $context3->{item}->{holdingbranch} + }); + my ( $borrowernumber, $session_id ) = + create_user_and_session( { authorized => 0 } ); + + my $tx = $t->ua->build_tx( GET => '/api/v1/issuingrules/effective' ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(401); + + $tx = $t->ua->build_tx( GET => '/api/v1/issuingrules/effective' ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(403); + + ( $borrowernumber, $session_id ) = + create_user_and_session( { authorized => 1 } ); + + my $path = '/api/v1/issuingrules/effective'; + my $params = {}; + + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => '*') + ->json_is('/itemtype' => '*'); + + $params = { + branchcode => '', + categorycode => '', + itemtype => '' + }; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => '*') + ->json_is('/itemtype' => '*'); + + $params = { itemtype => $context1->{item}->{itype} }; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => '*') + ->json_is('/itemtype' => '*'); + + $params = { + categorycode => $context1->{patron}->{categorycode}, + itemtype => $context1->{item}->{itype} + }; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => $context1->{patron}->{categorycode}) + ->json_is('/itemtype' => $context1->{item}->{itype}); + + subtest 'CircControl = ItemHomeLibrary' => sub { + plan tests => 15; + + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'homebranch'); + + create_issuing_rule({ + categorycode => $context3->{patron}->{categorycode}, + itemtype => $context3->{item}->{itype}, + branchcode => '*' + }); + + $params = { + cardnumber => $context3->{patron}->{cardnumber}, + barcode => $context3->{item}->{barcode} + }; + # Test exact match, item's homebranch + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => $context3->{item}->{homebranch}) + ->json_is('/categorycode' => $context3->{patron}->{categorycode}) + ->json_is('/itemtype' => $context3->{item}->{itype}); + + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'holdingbranch'); + $params = { + cardnumber => $context3->{patron}->{cardnumber}, + barcode => $context3->{item}->{barcode} + }; + # Test exact match, holdingbranch + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => $context3->{item}->{holdingbranch}) + ->json_is('/categorycode' => $context3->{patron}->{categorycode}) + ->json_is('/itemtype' => $context3->{item}->{itype}); + + # Test custom branch + $params->{'branchcode'} = $context2->{patron}->{branchcode}; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => $context3->{patron}->{categorycode}) + ->json_is('/itemtype' => $context3->{item}->{itype}); + }; + + subtest 'CircControl = PatronLibrary' => sub { + plan tests => 10; + + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + create_issuing_rule({ + categorycode => $context2->{patron}->{categorycode}, + itemtype => $context2->{item}->{itype}, + branchcode => '*' + }); + $params = { + itemnumber => $context2->{item}->{itemnumber}, + borrowernumber => $context2->{patron}->{borrowernumber} + }; + # Test exact match + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => $context2->{patron}->{branchcode}) + ->json_is('/categorycode' => $context2->{patron}->{categorycode}) + ->json_is('/itemtype' => $context2->{item}->{itype}); + + # Test custom branchcode + $params->{'branchcode'} = $context3->{patron}->{branchcode}; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => $context2->{patron}->{categorycode}) + ->json_is('/itemtype' => $context2->{item}->{itype}); + }; + + subtest 'CircControl = PickupLibrary' => sub { + plan tests => 15; + + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + my $patron = Koha::Patrons->find($borrowernumber); + create_issuing_rule({ + categorycode => $context2->{patron}->{categorycode}, + itemtype => $context2->{item}->{itype}, + branchcode => $patron->branchcode + }); + $params = { + cardnumber => $context2->{patron}->{cardnumber}, + barcode => $context2->{item}->{barcode} + }; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => $patron->branchcode) + ->json_is('/categorycode' => $context2->{patron}->{categorycode}) + ->json_is('/itemtype' => $context2->{item}->{itype}); + + $params = { + cardnumber => $context2->{patron}->{cardnumber}, + barcode => $context2->{item}->{barcode}, + branchcode => '', + }; + # Test all branches + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => $context2->{patron}->{categorycode}) + ->json_is('/itemtype' => $context2->{item}->{itype}); + + # Test another branch, should still return rule for all branches because + # there is no exact match to that branchcode + $params->{branchcode} = $context1->{patron}->{branchcode}; + $tx = $t->ua->build_tx( GET => _query_params($path, $params) ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is('/branchcode' => '*') + ->json_is('/categorycode' => $context2->{patron}->{categorycode}) + ->json_is('/itemtype' => $context2->{item}->{itype}); + }; + + $schema->storage->txn_rollback; +}; + +sub create_test_context { + my $itemtype = $builder->build({ source => 'Itemtype'}); + my $library = $builder->build({ source => 'Branch' }); + my $library2 = $builder->build({ source => 'Branch' }); + my $biblio = $builder->build({ source => 'Biblio' }); + my $biblioitem = $builder->build({ + source => 'Biblioitem', + value => { + biblionumber => $biblio->{biblionumber}, + itemtype => $itemtype->{itemtype}, + } + }); + my $item = $builder->build({ + source => 'Item', + value => { + biblionumber => $biblio->{biblionumber}, + biblioitemnumber => $biblioitem->{biblioitemnumber}, + itype => $itemtype->{itemtype}, + homebranch => $library->{branchcode}, + holdingbranch => $library2->{branchcode}, + } + }); + my $category = $builder->build({ source => 'Category' }); + my $patron = $builder->build({ + source => 'Borrower', + value => { + categorycode => $category->{categorycode} + } + }); + + return { + biblio => $biblio, + biblioitem => $biblioitem, + category => $category, + item => $item, + itemtype => $itemtype, + library => $library, + library_two => $library2, + patron => $patron, + }; +} + +sub create_issuing_rule { + my ($params) = @_; + + my $rule = $builder->build({ + source => 'Issuingrule', + value => $params + }); + + return $rule; +} +sub create_user_and_session { + + my $args = shift; + my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0; + my $dbh = C4::Context->dbh; + + my $user = $builder->build( + { + source => 'Borrower', + value => { + flags => $flags, + } + } + ); + + # Create a session for the authorized user + my $session = C4::Auth::get_session(''); + $session->param( 'number', $user->{borrowernumber} ); + $session->param( 'id', $user->{userid} ); + $session->param( 'ip', '127.0.0.1' ); + $session->param( 'lasttime', time() ); + $session->flush; + + if ( $args->{authorized} ) { + $dbh->do( " + INSERT INTO user_permissions (borrowernumber,module_bit,code) + VALUES (?,3,'manage_circ_rules')", undef, + $user->{borrowernumber} ); + } + + return ( $user->{borrowernumber}, $session->id ); +} + +sub _query_params { + my ($path, $params) = @_; + + $path .= '?'; + foreach my $param (keys %$params) { + $path .= $param.'='.$params->{$param}.'&'; + } + $path =~ s/\&$//; + return $path; +} +1; -- 2.7.4