From 3234615944d17b9530925ed998b7a83247a43e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kozlovsk=C3=BD?= Date: Sun, 31 Jul 2016 11:46:57 +0200 Subject: [PATCH] Bug 16497: Add /api/v1/libraries CRUD for libraries via REST API. GET /api/v1/libraries - List all libraries GET /api/v1/libraries/{branchcode} - Get one Library POST /api/v1/libraries - Add new Library DELETE /api/v1/libraries/{branchcode} - Delete Library Test plan: - apply patch - run tests: t/db_dependent/api/v1/libraries.t - test API with some API tool or simple curl e.g.: curl http://host:port/api/v1/libraries curl http://host:port/api/v1/libraries/cpl Signed-off-by: Josef Moravec Signed-off-by: Benjamin Rokseth Signed-off-by: Tomas Cohen Arazi Signed-off-by: Kyle M Hall Signed-off-by: Tomas Cohen Arazi Signed-off-by: Josef Moravec Signed-off-by: Martin Renvoize --- Koha/REST/V1/Library.pm | 141 ++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/library.json | 86 +++++ api/v1/swagger/parameters.json | 3 + api/v1/swagger/parameters/library.json | 9 + api/v1/swagger/paths.json | 6 + api/v1/swagger/paths/libraries.json | 362 +++++++++++++++++++++ api/v1/swagger/x-primitives.json | 6 + t/db_dependent/api/v1/libraries.t | 413 ++++++++++++++++++++++++ 9 files changed, 1029 insertions(+) create mode 100644 Koha/REST/V1/Library.pm create mode 100644 api/v1/swagger/definitions/library.json create mode 100644 api/v1/swagger/parameters/library.json create mode 100644 api/v1/swagger/paths/libraries.json create mode 100644 t/db_dependent/api/v1/libraries.t diff --git a/Koha/REST/V1/Library.pm b/Koha/REST/V1/Library.pm new file mode 100644 index 0000000000..97722179c5 --- /dev/null +++ b/Koha/REST/V1/Library.pm @@ -0,0 +1,141 @@ +package Koha::REST::V1::Library; + +# 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 Koha::Libraries; + +use Scalar::Util qw( blessed ); + +use Try::Tiny; + +sub list { + my $c = shift->openapi->valid_input or return; + + my $libraries; + my $filter; + my $args = $c->req->params->to_hash; + + for my $filter_param ( keys %$args ) { + $filter->{$filter_param} = { LIKE => $args->{$filter_param} . "%" }; + } + + return try { + my $libraries = Koha::Libraries->search($filter); + return $c->render( status => 200, openapi => $libraries ); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $_->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } + }; +} + +sub get { + my $c = shift->openapi->valid_input or return; + + my $branchcode = $c->validation->param('branchcode'); + my $library = Koha::Libraries->find({ branchcode => $branchcode }); + unless ($library) { + return $c->render( status => 404, + openapi => { error => "Library not found" } ); + } + + return $c->render( status => 200, openapi => $library ); +} + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + if (Koha::Libraries->find($c->req->json->{branchcode})) { + return $c->render( status => 400, + openapi => { error => 'Library already exists' } ); + } + my $library = Koha::Library->new($c->validation->param('body'))->store; + my $branchcode = $library->branchcode; + $c->res->headers->location($c->req->url->to_string.'/'.$branchcode); + return $c->render( status => 201, openapi => $library); + } + catch { + if ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $_->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } + }; +} + +sub update { + my $c = shift->openapi->valid_input or return; + + my $library; + return try { + $library = Koha::Libraries->find($c->validation->param('branchcode')); + $library->set($c->validation->param('body'))->store; + return $c->render( status => 200, openapi => $library ); + } + catch { + if ( not defined $library ) { + return $c->render( status => 404, + openapi => { error => "Object not found" }); + } + elsif ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $_->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } + }; +} + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $library; + return try { + $library = Koha::Libraries->find($c->validation->param('branchcode')); + $library->delete; + return $c->render( status => 204, openapi => ''); + } + catch { + if ( not defined $library ) { + return $c->render( status => 404, openapi => { error => "Object not found" } ); + } + elsif ( $_->isa('DBIx::Class::Exception') ) { + return $c->render( status => 500, + openapi => { error => $_->{msg} } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } + }; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 65094325c6..95f91e067d 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -14,6 +14,9 @@ "holds": { "$ref": "definitions/holds.json" }, + "library": { + "$ref": "definitions/library.json" + }, "patron": { "$ref": "definitions/patron.json" }, diff --git a/api/v1/swagger/definitions/library.json b/api/v1/swagger/definitions/library.json new file mode 100644 index 0000000000..e16390cdb0 --- /dev/null +++ b/api/v1/swagger/definitions/library.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "properties": { + "branchcode": { + "$ref": "../x-primitives.json#/branchcode" + }, + "branchname": { + "type": "string", + "description": "Printable name of library" + }, + "branchaddress1": { + "type": ["string", "null"], + "description": "the first address line of the library" + }, + "branchaddress2": { + "type": ["string", "null"], + "description": "the second address line of the library" + }, + "branchaddress3": { + "type": ["string", "null"], + "description": "the third address line of the library" + }, + "branchzip": { + "type": ["string", "null"], + "description": "the zip or postal code of the library" + }, + "branchcity": { + "type": ["string", "null"], + "description": "the city or province of the library" + }, + "branchstate": { + "type": ["string", "null"], + "description": "the reqional state of the library" + }, + "branchcountry": { + "type": ["string", "null"], + "description": "the county of the library" + }, + "branchphone": { + "type": ["string", "null"], + "description": "the primary phone of the library" + }, + "branchfax": { + "type": ["string", "null"], + "description": "the fax number of the library" + }, + "branchemail": { + "type": ["string", "null"], + "description": "the primary email address of the library" + }, + "branchreplyto": { + "type": ["string", "null"], + "description": "the email to be used as a Reply-To" + }, + "branchreturnpath": { + "type": ["string", "null"], + "description": "the email to be used as Return-Path" + }, + "branchurl": { + "type": ["string", "null"], + "description": "the URL for your library or branch's website" + }, + "issuing": { + "type": ["integer", "null"], + "description": "unused in Koha" + }, + "branchip": { + "type": ["string", "null"], + "description": "the IP address for your library or branch" + }, + "branchprinter": { + "type": ["string", "null"], + "description": "unused in Koha" + }, + "branchnotes": { + "type": ["string", "null"], + "description": "notes related to your library or branch" + }, + "opac_info": { + "type": ["string", "null"], + "description": "HTML that displays in OPAC" + } + }, + "additionalProperties": false, + "required": ["branchcode", "branchname"] +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index d951cf7d99..1d5feba907 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -8,6 +8,9 @@ "city_id_pp": { "$ref": "parameters/city.json#/city_id_pp" }, + "branchcodePathParam": { + "$ref": "parameters/library.json#/branchcodePathParam" + }, "holdIdPathParam": { "$ref": "parameters/hold.json#/holdIdPathParam" }, diff --git a/api/v1/swagger/parameters/library.json b/api/v1/swagger/parameters/library.json new file mode 100644 index 0000000000..366581e4e7 --- /dev/null +++ b/api/v1/swagger/parameters/library.json @@ -0,0 +1,9 @@ +{ + "branchcodePathParam": { + "name": "branchcode", + "in": "path", + "description": "Branch identifier code", + "required": true, + "type": "string" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index b41ef9e53a..077ebdd2b1 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -20,6 +20,12 @@ "/holds/{reserve_id}": { "$ref": "paths/holds.json#/~1holds~1{reserve_id}" }, + "/libraries": { + "$ref": "paths/libraries.json#/~1libraries" + }, + "/libraries/{branchcode}": { + "$ref": "paths/libraries.json#/~1libraries~1{branchcode}" + }, "/patrons": { "$ref": "paths/patrons.json#/~1patrons" }, diff --git a/api/v1/swagger/paths/libraries.json b/api/v1/swagger/paths/libraries.json new file mode 100644 index 0000000000..1457788163 --- /dev/null +++ b/api/v1/swagger/paths/libraries.json @@ -0,0 +1,362 @@ +{ + "/libraries": { + "get": { + "x-mojo-to": "Library#list", + "operationId": "listLibrary", + "tags": ["library"], + "parameters": [{ + "name": "branchname", + "in": "query", + "description": "Case insensitive 'starts-with' search on name", + "required": false, + "type": "string" + }, { + "name": "branchaddress1", + "in": "query", + "description": "Case insensitive 'starts-with' search on address1", + "required": false, + "type": "string" + }, { + "name": "branchaddress2", + "in": "query", + "description": "Case insensitive 'starts-with' search on address2", + "required": false, + "type": "string" + }, { + "name": "branchaddress3", + "in": "query", + "description": "Case insensitive 'starts-with' search on address3", + "required": false, + "type": "string" + }, { + "name": "branchzip", + "in": "query", + "description": "Case insensitive 'starts-with' search on zipcode", + "required": false, + "type": "string" + }, { + "name": "branchcity", + "in": "query", + "description": "Case insensitive 'starts-with' search on city", + "required": false, + "type": "string" + }, { + "name": "branchstate", + "in": "query", + "description": "Case insensitive 'starts-with' search on state", + "required": false, + "type": "string" + }, { + "name": "branchcountry", + "in": "query", + "description": "Case insensitive 'starts_with' search on country", + "required": false, + "type": "string" + }, { + "name": "branchphone", + "in": "query", + "description": "Case insensitive 'starts_with' search on phone number", + "required": false, + "type": "string" + }, { + "name": "branchfax", + "in": "query", + "description": "Case insensitive 'starts_with' search on fax number", + "required": false, + "type": "string" + }, { + "name": "branchemail", + "in": "query", + "description": "Case insensitive 'starts_with' search on email address", + "required": false, + "type": "string" + }, { + "name": "branchreplyto", + "in": "query", + "description": "Case insensitive 'starts_with' search on Reply-To email address", + "required": false, + "type": "string" + }, { + "name": "branchreturnpath", + "in": "query", + "description": "Case insensitive 'starts_with' search on Return-Path email address", + "required": false, + "type": "string" + }, { + "name": "branchurl", + "in": "query", + "description": "Case insensitive 'starts_with' search on website URL", + "required": false, + "type": "string" + }, { + "name": "issuing", + "in": "query", + "description": "Unused in Koha", + "required": false, + "type": "integer" + }, { + "name": "branchip", + "in": "query", + "description": "Case insensitive 'starts_with' search on IP address", + "required": false, + "type": "string" + }, { + "name": "branchprinter", + "in": "query", + "description": "Unused in Koha", + "required": false, + "type": "string" + }, { + "name": "branchnotes", + "in": "query", + "description": "Case insensitive 'starts_with' search on notes", + "required": false, + "type": "string" + }, { + "name": "opac_info", + "in": "query", + "description": "Case insensitive 'starts-with' search on OPAC info", + "required": false, + "type": "string" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A list of libraries", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/library" + } + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + }, + "post": { + "x-mojo-to": "Library#add", + "operationId": "addLibrary", + "tags": ["library"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new library", + "required": true, + "schema": { + "$ref": "../definitions.json#/library" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "Library added", + "schema": { + "$ref": "../definitions.json#/library" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "parameters": "parameters_remaining_permissions" + } + } + } + }, + "/libraries/{branchcode}": { + "get": { + "x-mojo-to": "Library#get", + "operationId": "getLibrary", + "tags": ["library"], + "parameters": [ + { + "$ref": "../parameters.json#/branchcodePathParam" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A library", + "schema": { + "$ref": "../definitions.json#/library" + } + }, + "404": { + "description": "Library not found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + }, + "put": { + "x-mojo-to": "Library#update", + "operationId": "updateLibrary", + "tags": ["library"], + "parameters": [{ + "$ref": "../parameters.json#/branchcodePathParam" + }, { + "name": "body", + "in": "body", + "description": "A JSON object containing information on the library", + "required": true, + "schema": { + "$ref": "../definitions.json#/library" + } + }], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A library", + "schema": { + "$ref": "../definitions.json#/library" + } + }, + "400": { + "description": "Bad request", + "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": "Library not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "parameters": "parameters_remaining_permissions" + } + } + }, + "delete": { + "x-mojo-to": "Library#delete", + "operationId": "deleteLibrary", + "tags": ["library"], + "parameters": [{ + "$ref": "../parameters.json#/branchcodePathParam" + }], + "produces": [ + "application/json" + ], + "responses": { + "204": { + "description": "Library deleted", + "schema": { "type": "string" } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Library not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "parameters": "parameters_remaining_permissions" + } + } + } + } +} diff --git a/api/v1/swagger/x-primitives.json b/api/v1/swagger/x-primitives.json index 0266297cee..9b077d6f8f 100644 --- a/api/v1/swagger/x-primitives.json +++ b/api/v1/swagger/x-primitives.json @@ -7,6 +7,12 @@ "type": "integer", "description": "Internal patron identifier" }, + "branchcode": { + "type": "string", + "description": "internally assigned library identifier", + "maxLength": 10, + "minLength": 1 + }, "cardnumber": { "type": ["string", "null"], "description": "library assigned user identifier" diff --git a/t/db_dependent/api/v1/libraries.t b/t/db_dependent/api/v1/libraries.t new file mode 100644 index 0000000000..7b59610c77 --- /dev/null +++ b/t/db_dependent/api/v1/libraries.t @@ -0,0 +1,413 @@ +#!/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 => 5; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; +use Koha::Libraries; +use Koha::Database; + +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 'list() tests' => sub { + plan tests => 8; + + $schema->storage->txn_begin; + + # Create test context + my $library = $builder->build( { source => 'Branch' } ); + my $another_library = { %$library }; # create a copy of $library but make + delete $another_library->{branchcode}; # sure branchcode will be regenerated + $another_library = $builder->build( + { source => 'Branch', value => $another_library } ); + my ( $borrowernumber, $session_id ) = + create_user_and_session( { authorized => 0 } ); + + ## Authorized user tests + my $count_of_libraries = Koha::Libraries->search->count; + # Make sure we are returned with the correct amount of libraries + my $tx = $t->ua->build_tx( GET => '/api/v1/libraries' ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_has('/'.($count_of_libraries-1).'/branchcode') + ->json_hasnt('/'.($count_of_libraries).'/branchcode'); + + subtest 'query parameters' => sub { + my @fields = qw( + branchname branchaddress1 branchaddress2 branchaddress3 + branchzip branchcity branchstate branchcountry + branchphone branchfax branchemail branchreplyto + branchreturnpath branchurl issuing branchip + branchprinter branchnotes opac_info + ); + plan tests => scalar(@fields)*3; + + foreach my $field (@fields) { + $tx = $t->ua->build_tx( GET => + "/api/v1/libraries?$field=$library->{$field}" ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + my $result = + $t->request_ok($tx) + ->status_is(200) + ->json_has( [ $library, $another_library ] ); + } + }; + + # Warn on unsupported query parameter + $tx = $t->ua->build_tx( GET => '/api/v1/libraries?library_blah=blah' ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_is( [{ path => '/query/library_blah', message => 'Malformed query string'}] ); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 6; + + $schema->storage->txn_begin; + + my $library = $builder->build( { source => 'Branch' } ); + my ( $borrowernumber, $session_id ) = + create_user_and_session( { authorized => 0 } ); + + my $tx = $t->ua->build_tx( GET => "/api/v1/libraries/" . $library->{branchcode} ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is($library); + + my $non_existent_code = 'non_existent'.int(rand(10000)); + $tx = $t->ua->build_tx( GET => "/api/v1/libraries/" . $non_existent_code ); + $tx->req->cookies( { name => 'CGISESSID', value => $session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(404) + ->json_is( '/error' => 'Library not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + plan tests => 31; + + $schema->storage->txn_begin; + + my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = + create_user_and_session( { authorized => 0 } ); + my ( $authorized_borrowernumber, $authorized_session_id ) = + create_user_and_session( { authorized => 1 } ); + my $library = { + branchcode => "LIBRARYBR1", + branchname => "Library Name", + branchaddress1 => "Library Address1", + branchaddress2 => "Library Address2", + branchaddress3 => "Library Address3", + branchzip => "Library Zipcode", + branchcity => "Library City", + branchstate => "Library State", + branchcountry => "Library Country", + branchphone => "Library Phone", + branchfax => "Library Fax", + branchemail => "Library Email", + branchreplyto => "Library Reply-To", + branchreturnpath => "Library Return-Path", + branchurl => "http://library.url", + issuing => undef, # unused in Koha + branchip => "127.0.0.1", + branchprinter => "Library Printer", # unused in Koha + branchnotes => "Library Notes", + opac_info => "

Library OPAC info

", + }; + + # Unauthorized attempt to write + my $tx = $t->ua->build_tx( POST => "/api/v1/libraries" => json => $library ); + $tx->req->cookies( + { name => 'CGISESSID', value => $unauthorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(403); + + # Authorized attempt to write invalid data + my $library_with_invalid_field = { %$library }; + $library_with_invalid_field->{'branchinvalid'} = 'Library invalid'; + + $tx = $t->ua->build_tx( + POST => "/api/v1/libraries" => json => $library_with_invalid_field ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: branchinvalid.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + $tx = $t->ua->build_tx( POST => "/api/v1/libraries" => json => $library ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + my $branchcode = $t->request_ok($tx) + ->status_is(201) + ->json_is( '/branchname' => $library->{branchname} ) + ->json_is( '/branchaddress1' => $library->{branchaddress1} ) + ->json_is( '/branchaddress2' => $library->{branchaddress2} ) + ->json_is( '/branchaddress3' => $library->{branchaddress3} ) + ->json_is( '/branchzip' => $library->{branchzip} ) + ->json_is( '/branchcity' => $library->{branchcity} ) + ->json_is( '/branchstate' => $library->{branchstate} ) + ->json_is( '/branchcountry' => $library->{branchcountry} ) + ->json_is( '/branchphone' => $library->{branchphone} ) + ->json_is( '/branchfax' => $library->{branchfax} ) + ->json_is( '/branchemail' => $library->{branchemail} ) + ->json_is( '/branchreplyto' => $library->{branchreplyto} ) + ->json_is( '/branchreturnpath' => $library->{branchreturnpath} ) + ->json_is( '/branchurl' => $library->{branchurl} ) + ->json_is( '/branchip' => $library->{branchip} ) + ->json_is( '/branchnotes' => $library->{branchnotes} ) + ->json_is( '/opac_info' => $library->{opac_info} ) + ->header_is(Location => "/api/v1/libraries/$library->{branchcode}") + ->tx->res->json->{branchcode}; + + # Authorized attempt to create with null id + $library->{branchcode} = undef; + $tx = $t->ua->build_tx( + POST => "/api/v1/libraries" => json => $library ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_has('/errors'); + + # Authorized attempt to create with existing id + $library->{branchcode} = $branchcode; + $tx = $t->ua->build_tx( + POST => "/api/v1/libraries" => json => $library ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_is('/error' => 'Library already exists'); + + $schema->storage->txn_rollback; +}; + +subtest 'update() tests' => sub { + plan tests => 13; + + $schema->storage->txn_begin; + + my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = + create_user_and_session( { authorized => 0 } ); + my ( $authorized_borrowernumber, $authorized_session_id ) = + create_user_and_session( { authorized => 1 } ); + + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + + # Unauthorized attempt to update + my $tx = $t->ua->build_tx( PUT => "/api/v1/libraries/$branchcode" + => json => { branchname => 'New unauthorized name change' } ); + $tx->req->cookies( + { name => 'CGISESSID', value => $unauthorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(403); + + # Attempt partial update on a PUT + my $library_with_missing_field = { + branchaddress1 => "New library address", + }; + + $tx = $t->ua->build_tx( PUT => "/api/v1/libraries/$branchcode" => + json => $library_with_missing_field ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_has( "/errors" => + [ { message => "Missing property.", path => "/body/branchaddress2" } ] + ); + + # Full object update on PUT + my $library_with_updated_field = { + branchcode => "LIBRARYBR2", + branchname => "Library Name", + branchaddress1 => "Library Address1", + branchaddress2 => "Library Address2", + branchaddress3 => "Library Address3", + branchzip => "Library Zipcode", + branchcity => "Library City", + branchstate => "Library State", + branchcountry => "Library Country", + branchphone => "Library Phone", + branchfax => "Library Fax", + branchemail => "Library Email", + branchreplyto => "Library Reply-To", + branchreturnpath => "Library Return-Path", + branchurl => "http://library.url", + issuing => undef, # unused in Koha + branchip => "127.0.0.1", + branchprinter => "Library Printer", # unused in Koha + branchnotes => "Library Notes", + opac_info => "

Library OPAC info

", + }; + + $tx = $t->ua->build_tx( + PUT => "/api/v1/libraries/$branchcode" => json => $library_with_updated_field ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(200) + ->json_is( '/branchname' => 'Library Name' ); + + # Authorized attempt to write invalid data + my $library_with_invalid_field = { %$library_with_updated_field }; + $library_with_invalid_field->{'branchinvalid'} = 'Library invalid'; + + $tx = $t->ua->build_tx( + PUT => "/api/v1/libraries/$branchcode" => json => $library_with_invalid_field ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: branchinvalid.", + path => "/body" + } + ] + ); + + my $non_existent_code = 'nope'.int(rand(10000)); + $tx = + $t->ua->build_tx( PUT => "/api/v1/libraries/$non_existent_code" => json => + $library_with_updated_field ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(404); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = + create_user_and_session( { authorized => 0 } ); + my ( $authorized_borrowernumber, $authorized_session_id ) = + create_user_and_session( { authorized => 1 } ); + + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + + # Unauthorized attempt to delete + my $tx = $t->ua->build_tx( DELETE => "/api/v1/libraries/$branchcode" ); + $tx->req->cookies( + { name => 'CGISESSID', value => $unauthorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(403); + + $tx = $t->ua->build_tx( DELETE => "/api/v1/libraries/$branchcode" ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(204) + ->content_is(''); + + $tx = $t->ua->build_tx( DELETE => "/api/v1/libraries/$branchcode" ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx) + ->status_is(404); + + $schema->storage->txn_rollback; +}; + +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,'parameters_remaining_permissions')", undef, + $user->{borrowernumber} ); + } + + return ( $user->{borrowernumber}, $session->id ); +} + +1; -- 2.20.1