From a0a5092c92945786cebf603a9fdeb010f5dad1f8 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 8 Nov 2019 14:20:37 +0000 Subject: [PATCH] Bug 23998: REST API for messages - Add /api/v1/messages endpoint Exposes messages database table via REST API. GET /api/v1/messages POST /api/v1/messages PUT /api/v1/messages DELETE /api/v1/messages To test: 1. prove t/db_dependent/api/v1/messages.t Sponsored-by: Koha-Suomi Oy --- Koha/Patron/Message.pm | 19 ++ Koha/REST/V1/Messages.pm | 255 +++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/message.json | 37 +++ api/v1/swagger/parameters.json | 3 + api/v1/swagger/parameters/message.json | 9 + api/v1/swagger/paths.json | 6 + api/v1/swagger/paths/messages.json | 327 +++++++++++++++++++ api/v1/swagger/x-primitives.json | 5 + t/db_dependent/api/v1/messages.t | 403 ++++++++++++++++++++++++ 10 files changed, 1067 insertions(+) create mode 100644 Koha/REST/V1/Messages.pm create mode 100644 api/v1/swagger/definitions/message.json create mode 100644 api/v1/swagger/parameters/message.json create mode 100644 api/v1/swagger/paths/messages.json create mode 100644 t/db_dependent/api/v1/messages.t diff --git a/Koha/Patron/Message.pm b/Koha/Patron/Message.pm index 838378011b..4a500d134f 100644 --- a/Koha/Patron/Message.pm +++ b/Koha/Patron/Message.pm @@ -73,6 +73,25 @@ sub delete { return $self->SUPER::delete($self); } +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::Patron::Message object +on the API. + +=cut + +sub to_api_mapping { + return { + message_id => 'message_id', + borrowernumber => 'patron_id', + branchcode => 'library_id', + message_type => 'message_type', + message => 'message', + message_date => 'message_date', + manager_id => 'manager_id', + }; +} + =head3 _type =cut diff --git a/Koha/REST/V1/Messages.pm b/Koha/REST/V1/Messages.pm new file mode 100644 index 0000000000..5ea52b1e8e --- /dev/null +++ b/Koha/REST/V1/Messages.pm @@ -0,0 +1,255 @@ +package Koha::REST::V1::Messages; + +# 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::Patron::Messages; + +use Try::Tiny; + +=head1 API + +=head2 Class Methods + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $messages_set = Koha::Patron::Messages->new; + my $messages = $c->objects->search( $messages_set, \&_to_model, \&_to_api ); + return $c->render( status => 200, openapi => $messages ); + } + 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."} ); + } + }; + +} + +=head3 get + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + my $message = Koha::Patron::Messages->find( $c->validation->param('message_id') ); + unless ($message) { + return $c->render( status => 404, + openapi => { error => "Message not found" } ); + } + + return $c->render( status => 200, openapi => $message->to_api ); +} + +=head3 add + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + my $message = Koha::Patron::Message->new( _to_model( $c->validation->param('body') ) ); + my $user = $c->stash('koha.user'); + $message->set({ manager_id => $user->borrowernumber }) unless defined $message->manager_id; + $message->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $message->message_id ); + return $c->render( + status => 201, + openapi => $message->to_api + ); + } + 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." } + ); + } + }; +} + +=head3 update + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $message = Koha::Patron::Messages->find( $c->validation->param('message_id') ); + + if ( not defined $message ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + my $params = $c->req->json; + $message->set( _to_model($params) ); + $message->store(); + return $c->render( status => 200, openapi => $message->to_api ); + } + catch { + if ( $_->isa('Koha::Exceptions::Object') ) { + return $c->render( status => 500, + openapi => { error => $_->message } ); + } + else { + return $c->render( status => 500, + openapi => { error => "Something went wrong, check the logs."} ); + } + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $message = Koha::Patron::Messages->find( $c->validation->param('message_id') ); + if ( not defined $message ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + $message->delete; + return $c->render( status => 200, openapi => "" ); + } + 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."} ); + } + }; +} + +=head3 _to_api + +Helper function that maps a hashref of Koha::Patron::Message attributes into REST api +attribute names. + +=cut + +sub _to_api { + my $message = shift; + + # Rename attributes + foreach my $column ( keys %{ $Koha::REST::V1::Messages::to_api_mapping } ) { + my $mapped_column = $Koha::REST::V1::Messages::to_api_mapping->{$column}; + if ( exists $message->{ $column } + && defined $mapped_column ) + { + # key /= undef + $message->{ $mapped_column } = delete $message->{ $column }; + } + elsif ( exists $message->{ $column } + && !defined $mapped_column ) + { + # key == undef => to be deleted + delete $message->{ $column }; + } + } + + return $message; +} + +=head3 _to_model + +Helper function that maps REST api objects into Koha::Patron::Messages +attribute names. + +=cut + +sub _to_model { + my $message = shift; + + foreach my $attribute ( keys %{ $Koha::REST::V1::Messages::to_model_mapping } ) { + my $mapped_attribute = $Koha::REST::V1::Messages::to_model_mapping->{$attribute}; + if ( exists $message->{ $attribute } + && defined $mapped_attribute ) + { + # key /= undef + $message->{ $mapped_attribute } = delete $message->{ $attribute }; + } + elsif ( exists $message->{ $attribute } + && !defined $mapped_attribute ) + { + # key == undef => to be deleted + delete $message->{ $attribute }; + } + } + + return $message; +} + +=head2 Global variables + +=head3 $to_api_mapping + +=cut + +our $to_api_mapping = { + message_id => 'message_id', + borrowernumber => 'patron_id', + branchcode => 'library_id', + message_type => 'message_type', + message => 'message', + message_date => 'message_date', + manager_id => 'manager_id', +}; + +=head3 $to_model_mapping + +=cut + +our $to_model_mapping = { + message_id => 'message_id', + patron_id => 'borrowernumber', + library_id => 'branchcode', + message_type => 'message_type', + message => 'message', + message_date => 'message_date', + manager_id => 'manager_id', +}; + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index 90374145a7..1542eabe44 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -23,6 +23,9 @@ "library": { "$ref": "definitions/library.json" }, + "message": { + "$ref": "definitions/message.json" + }, "item": { "$ref": "definitions/item.json" }, diff --git a/api/v1/swagger/definitions/message.json b/api/v1/swagger/definitions/message.json new file mode 100644 index 0000000000..8677280c6a --- /dev/null +++ b/api/v1/swagger/definitions/message.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "properties": { + "message_id": { + "$ref": "../x-primitives.json#/message_id" + }, + "patron_id": { + "$ref": "../x-primitives.json#/patron_id" + }, + "library_id": { + "type": ["string", "null"], + "description": "Internally assigned library identifier", + "maxLength": 10, + "minLength": 1 + }, + "message_type": { + "description": "One of following values: L = For Librarians, B = For Patrons", + "type": "string", + "maxLength": 1 + }, + "message": { + "description": "Message content", + "type": "string" + }, + "message_date": { + "description": "Message content", + "format": "date-time", + "type": "string" + }, + "manager_id": { + "type": ["integer", "null"], + "description": "Internal patron identifier for message manager" + } + }, + "additionalProperties": false, + "required": ["patron_id", "message_type", "message"] +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 932d251d42..b6f528fb15 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -20,6 +20,9 @@ "library_id_pp": { "$ref": "parameters/library.json#/library_id_pp" }, + "message_id_pp": { + "$ref": "parameters/message.json#/message_id_pp" + }, "item_id_pp": { "$ref": "parameters/item.json#/item_id_pp" }, diff --git a/api/v1/swagger/parameters/message.json b/api/v1/swagger/parameters/message.json new file mode 100644 index 0000000000..63468da2e9 --- /dev/null +++ b/api/v1/swagger/parameters/message.json @@ -0,0 +1,9 @@ +{ + "message_id_pp": { + "name": "message_id", + "in": "path", + "description": "Message internal identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 95278ec23a..c152e072b2 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -56,6 +56,12 @@ "/libraries/{library_id}": { "$ref": "paths/libraries.json#/~1libraries~1{library_id}" }, + "/messages": { + "$ref": "paths/messages.json#/~1messages" + }, + "/messages/{message_id}": { + "$ref": "paths/messages.json#/~1messages~1{message_id}" + }, "/checkouts/{checkout_id}/allows_renewal": { "$ref": "paths/checkouts.json#/~1checkouts~1{checkout_id}~1allows_renewal" }, diff --git a/api/v1/swagger/paths/messages.json b/api/v1/swagger/paths/messages.json new file mode 100644 index 0000000000..f158441ab7 --- /dev/null +++ b/api/v1/swagger/paths/messages.json @@ -0,0 +1,327 @@ +{ + "/messages": { + "get": { + "x-mojo-to": "Messages#list", + "operationId": "listmessages", + "tags": ["messages"], + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "patron_id", + "in": "query", + "description": "Search on patron id", + "required": false, + "type": "integer" + }, { + "name": "library_id", + "in": "query", + "description": "Case insensitive search on library id", + "required": false, + "type": "string" + }, { + "name": "message_type", + "in": "query", + "description": "Case insensitive search on message type", + "required": false, + "type": "string" + }, { + "name": "message", + "in": "query", + "description": "Case Insensitive search on message content", + "required": false, + "type": "string" + }, { + "name": "message_date", + "in": "query", + "description": "Case Insensitive search on message date", + "required": false, + "type": "string" + }, { + "name": "manager_id", + "in": "query", + "description": "Case Insensitive search on manager patron id", + "required": false, + "type": "integer" + }], + "responses": { + "200": { + "description": "A list of messages", + "schema": { + "type": "array", + "items": { + "$ref": "../definitions.json#/message" + } + } + }, + "400": { + "description": "Bad request", + "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": { + "borrowers": "1" + } + } + }, + "post": { + "x-mojo-to": "Messages#add", + "operationId": "addmessage", + "tags": ["messages"], + "parameters": [{ + "name": "body", + "in": "body", + "description": "A JSON object containing informations about the new hold", + "required": true, + "schema": { + "$ref": "../definitions.json#/message" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "message added", + "schema": { + "$ref": "../definitions.json#/message" + } + }, + "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": { + "borrowers": "edit_borrowers" + } + } + } + }, + "/messages/{message_id}": { + "get": { + "x-mojo-to": "Messages#get", + "operationId": "getmessage", + "tags": ["messages"], + "parameters": [{ + "$ref": "../parameters.json#/message_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A message", + "schema": { + "$ref": "../definitions.json#/message" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "message 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": { + "borrowers": "1" + } + } + }, + "put": { + "x-mojo-to": "Messages#update", + "operationId": "updatemessage", + "tags": ["messages"], + "parameters": [{ + "$ref": "../parameters.json#/message_id_pp" + }, { + "name": "body", + "in": "body", + "description": "A message object", + "required": true, + "schema": { + "$ref": "../definitions.json#/message" + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "A message", + "schema": { + "$ref": "../definitions.json#/message" + } + }, + "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": "message 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": { + "borrowers": "edit_borrowers" + } + } + }, + "delete": { + "x-mojo-to": "Messages#delete", + "operationId": "deletemessage", + "tags": ["messages"], + "parameters": [{ + "$ref": "../parameters.json#/message_id_pp" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "message deleted", + "schema": { + "type": "string" + } + }, + "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": "message 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": { + "borrowers": "edit_borrowers" + } + } + } + } +} diff --git a/api/v1/swagger/x-primitives.json b/api/v1/swagger/x-primitives.json index ee15fa6aa1..7ee33f11fa 100644 --- a/api/v1/swagger/x-primitives.json +++ b/api/v1/swagger/x-primitives.json @@ -26,6 +26,11 @@ "type": ["string", "null"], "description": "primary email address for patron's primary address" }, + "message_id": { + "type": "integer", + "description": "internally assigned message identifier", + "readOnly": true + }, "firstname": { "type": ["string", "null"], "description": "patron's first name" diff --git a/t/db_dependent/api/v1/messages.t b/t/db_dependent/api/v1/messages.t new file mode 100644 index 0000000000..70f746c745 --- /dev/null +++ b/t/db_dependent/api/v1/messages.t @@ -0,0 +1,403 @@ +#!/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 t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::Patron::Messages; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +my $t = Test::Mojo->new('Koha::REST::V1'); +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +subtest 'list() tests' => sub { + + plan tests => 20; + + $schema->storage->txn_begin; + + Koha::Patron::Messages->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4 ** 2 } # borrowers flag = 4 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + ## Authorized user tests + # No messages, so empty array should be returned + $t->get_ok("//$userid:$password@/api/v1/messages") + ->status_is(200) + ->json_is( [] ); + + my $message = $builder->build_object({ class => 'Koha::Patron::Messages' }); + + # One message created, should get returned + $t->get_ok("//$userid:$password@/api/v1/messages") + ->status_is(200) + ->json_is( [Koha::REST::V1::Messages::_to_api( $message->TO_JSON )] ); + + my $another_message = $builder->build_object( + { class => 'Koha::Patron::Messages', value => { message_type => $message->message_type } } ); + my $message_with_another_message_type = $builder->build_object({ class => 'Koha::Patron::Messages' }); + + # Two messages created, they should both be returned + $t->get_ok("//$userid:$password@/api/v1/messages") + ->status_is(200) + ->json_is([Koha::REST::V1::Messages::_to_api($message->TO_JSON), + Koha::REST::V1::Messages::_to_api($another_message->TO_JSON), + Koha::REST::V1::Messages::_to_api($message_with_another_message_type->TO_JSON) + ] ); + + # Filtering works, two messages sharing message_type + $t->get_ok("//$userid:$password@/api/v1/messages?message_type=" . $message->message_type ) + ->status_is(200) + ->json_is([ Koha::REST::V1::Messages::_to_api($message->TO_JSON), + Koha::REST::V1::Messages::_to_api($another_message->TO_JSON) + ]); + + $t->get_ok("//$userid:$password@/api/v1/messages?message=" . $message->message ) + ->status_is(200) + ->json_is( [Koha::REST::V1::Messages::_to_api($message->TO_JSON)] ); + + # Warn on unsupported query parameter + $t->get_ok("//$userid:$password@/api/v1/messages?message_blah=blah" ) + ->status_is(400) + ->json_is( [{ path => '/query/message_blah', message => 'Malformed query string'}] ); + + # Unauthorized access + $t->get_ok("//$unauth_userid:$password@/api/v1/messages") + ->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $message = $builder->build_object({ class => 'Koha::Patron::Messages' }); + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4**2 } # borrowers flag = 4 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + $t->get_ok( "//$userid:$password@/api/v1/messages/" . $message->message_id ) + ->status_is(200) + ->json_is(Koha::REST::V1::Messages::_to_api($message->TO_JSON)); + + $t->get_ok( "//$unauth_userid:$password@/api/v1/messages/" . $message->message_id ) + ->status_is(403); + + my $message_to_delete = $builder->build_object({ class => 'Koha::Patron::Messages' }); + my $non_existent_id = $message_to_delete->id; + $message_to_delete->delete; + + $t->get_ok( "//$userid:$password@/api/v1/messages/$non_existent_id" ) + ->status_is(404) + ->json_is( '/error' => 'Message not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 27; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4**2 } # borrowers flag = 4 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $message = { + patron_id => $patron->borrowernumber, + library_id => $patron->branchcode, + message_type => "B", + message => "Old Fox jumped over Cheeseboy" + }; + + # Unauthorized attempt to write + $t->post_ok("//$unauth_userid:$password@/api/v1/messages" => json => $message) + ->status_is(403); + + # Authorized attempt to write invalid data + my $message_with_invalid_field = { + blah => "message Blah", + patron_id => $patron->borrowernumber, + library_id => $patron->branchcode, + message_type => "B", + message => "Old Fox jumped over Cheeseboy", + manager_id => $librarian->borrowernumber + }; + + $t->post_ok( "//$userid:$password@/api/v1/messages" => json => $message_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + my $message_id = + $t->post_ok( "//$userid:$password@/api/v1/messages" => json => $message ) + ->status_is( 201, 'SWAGGER3.2.1' ) + ->header_like( + Location => qr|^\/api\/v1\/messages/\d*|, + 'SWAGGER3.4.1' + ) + ->json_is( '/patron_id' => $message->{patron_id} ) + ->json_is( '/library_id' => $message->{library_id} ) + ->json_is( '/message_type' => $message->{message_type} ) + ->json_is( '/message' => $message->{message} ) + ->json_is( '/manager_id' => $librarian->borrowernumber ) + ->tx->res->json->{message_id}; + + # Authorized attempt to write with manager_id defined + $message->{manager_id} = $message->{patron_id}; + $message_id = + $t->post_ok( "//$userid:$password@/api/v1/messages" => json => $message ) + ->status_is( 201, 'SWAGGER3.2.1' ) + ->header_like( + Location => qr|^\/api\/v1\/messages/\d*|, + 'SWAGGER3.4.1' + ) + ->json_is( '/patron_id' => $message->{patron_id} ) + ->json_is( '/library_id' => $message->{library_id} ) + ->json_is( '/message_type' => $message->{message_type} ) + ->json_is( '/message' => $message->{message} ) + ->json_is( '/manager_id' => $message->{patron_id} ) + ->tx->res->json->{message_id}; + + # Authorized attempt to create with null id + $message->{message_id} = undef; + $t->post_ok( "//$userid:$password@/api/v1/messages" => json => $message ) + ->status_is(400) + ->json_has('/errors'); + + # Authorized attempt to create with existing id + $message->{message_id} = $message_id; + $t->post_ok( "//$userid:$password@/api/v1/messages" => json => $message ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Read-only.", + path => "/body/message_id" + } + ] + ); + + $schema->storage->txn_rollback; +}; + +subtest 'update() tests' => sub { + + plan tests => 15; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4**2 } # borrowers flag = 4 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $message_id = $builder->build_object({ class => 'Koha::Patron::Messages' } )->id; + + # Unauthorized attempt to update + $t->put_ok( "//$unauth_userid:$password@/api/v1/messages/$message_id" => json => { name => 'New unauthorized name change' } ) + ->status_is(403); + + # Attempt partial update on a PUT + my $message_with_missing_field = { + patron_id => $patron->borrowernumber, + library_id => $patron->branchcode, + message => "Old Fox jumped over Cheeseboy", + manager_id => $librarian->borrowernumber + }; + + $t->put_ok( "//$userid:$password@/api/v1/messages/$message_id" => json => $message_with_missing_field ) + ->status_is(400) + ->json_is( "/errors" => + [ { message => "Missing property.", path => "/body/message_type" } ] + ); + + # Full object update on PUT + my $message_with_updated_field = { + patron_id => $patron->borrowernumber, + library_id => $patron->branchcode, + message_type => "B", + message => "Old Fox jumped over Cheeseboy", + manager_id => $librarian->borrowernumber + }; + + $t->put_ok( "//$userid:$password@/api/v1/messages/$message_id" => json => $message_with_updated_field ) + ->status_is(200) + ->json_is( '/message' => 'Old Fox jumped over Cheeseboy' ); + + # Authorized attempt to write invalid data + my $message_with_invalid_field = { + blah => "message Blah", + patron_id => $patron->borrowernumber, + library_id => $patron->branchcode, + message_type => "B", + message => "Old Fox jumped over Cheeseboy", + manager_id => $librarian->borrowernumber + }; + + $t->put_ok( "//$userid:$password@/api/v1/messages/$message_id" => json => $message_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + my $message_to_delete = $builder->build_object({ class => 'Koha::Patron::Messages' }); + my $non_existent_id = $message_to_delete->id; + $message_to_delete->delete; + + $t->put_ok( "//$userid:$password@/api/v1/messages/$non_existent_id" => json => $message_with_updated_field ) + ->status_is(404); + + # Wrong method (POST) + $message_with_updated_field->{message_id} = 2; + + $t->post_ok( "//$userid:$password@/api/v1/messages/$message_id" => json => $message_with_updated_field ) + ->status_is(404); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + + plan tests => 7; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 4**2 } # borrowers flag = 4 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $message_id = $builder->build_object({ class => 'Koha::Patron::Messages' })->id; + + # Unauthorized attempt to delete + $t->delete_ok( "//$unauth_userid:$password@/api/v1/messages/$message_id" ) + ->status_is(403); + + $t->delete_ok("//$userid:$password@/api/v1/messages/$message_id") + ->status_is(200) + ->content_is('""'); + + $t->delete_ok("//$userid:$password@/api/v1/messages/$message_id") + ->status_is(404); + + $schema->storage->txn_rollback; +}; -- 2.17.1