From 7ff782188759fbb52de4e563063e7f1bb55852c2 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Fri, 12 Jun 2020 15:20:05 +0200 Subject: [PATCH] Bug 17390: Add /authorised_values endpoint This patch add the different routes for authorised values: * GET /authorised_values * GET /authorised_values/{authorised_value_id} * POST /authorised_values * PUT /authorised_values/{authorised_value_id} * DELETE /authorised_values/{authorised_value_id} Test plan: - Make sure the tests in t/db_dependent/api/v1/authorised_values.t pass - Test the different routes. For instance: GET /authorised_values # list all the avs GET /authorised_values?category=YES_NO # list all the YES_NO avs POST /authorised_values { "category": "YES_NO", "description": "not sure", "opac_description": "something else", "value": "maybe" } DELETE /authorised_values/X # with X the AV id of "maybe" Sponsored-by: Orex Digital --- Koha/REST/V1/AuthorisedValues.pm | 109 ++++++ api/v1/swagger/paths/authorised_values.yaml | 216 +++++++++++- api/v1/swagger/swagger.yaml | 4 + t/db_dependent/api/v1/authorised_values.t | 352 +++++++++++++++++++- 4 files changed, 679 insertions(+), 2 deletions(-) diff --git a/Koha/REST/V1/AuthorisedValues.pm b/Koha/REST/V1/AuthorisedValues.pm index 3a4706f6681..8fe3522f6c3 100644 --- a/Koha/REST/V1/AuthorisedValues.pm +++ b/Koha/REST/V1/AuthorisedValues.pm @@ -20,6 +20,7 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; use Koha::AuthorisedValueCategories; +use Koha::AuthorisedValues; use Try::Tiny; @@ -52,7 +53,115 @@ sub list_av_from_category { } catch { $c->unhandled_exception($_); }; +} + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $av_set = Koha::AuthorisedValues->new; + my $avs = $c->objects->search( $av_set ); + return $c->render( status => 200, openapi => $avs ); + } + catch { + $c->unhandled_exception($_); + }; + +} + +=head3 get + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $av = Koha::AuthorisedValues->find( $c->validation->param('authorised_value_id') ); + unless ($av) { + return $c->render( status => 404, + openapi => { error => "Authorised value not found" } ); + } + + return $c->render( status => 200, openapi => $av->to_api ); + } + catch { + $c->unhandled_exception($_); + } +} + +=head3 add + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + my $av = Koha::AuthorisedValue->new_from_api( $c->validation->param('body') ); + $av->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $av->id ); + return $c->render( + status => 201, + openapi => $av->to_api + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 update + +=cut +sub update { + my $c = shift->openapi->valid_input or return; + + my $av = Koha::AuthorisedValues->find( $c->validation->param('authorised_value_id') ); + + if ( not defined $av ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + $av->set_from_api( $c->validation->param('body') ); + $av->store(); + return $c->render( status => 200, openapi => $av->to_api ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $av = Koha::AuthorisedValues->find( $c->validation->param('authorised_value_id') ); + if ( not defined $av ) { + return $c->render( status => 404, + openapi => { error => "Object not found" } ); + } + + return try { + $av->delete; + return $c->render( + status => 204, + openapi => q{} + ); + } + catch { + $c->unhandled_exception($_); + }; } 1; diff --git a/api/v1/swagger/paths/authorised_values.yaml b/api/v1/swagger/paths/authorised_values.yaml index 07e298dc217..c3f0821975a 100644 --- a/api/v1/swagger/paths/authorised_values.yaml +++ b/api/v1/swagger/paths/authorised_values.yaml @@ -1,4 +1,218 @@ --- +/authorised_values: + get: + operationId: listAuthorisedValues + parameters: + - description: Search authorised values by category + in: query + name: category + required: false + type: string + - description: Search authorised values by value + in: query + name: value + required: false + type: string + - description: Search authorised values by description + in: query + name: description + required: false + type: string + - description: Search authorised values by opac description + in: query + name: opac_description + required: false + type: string + - description: Search authorised values by image url + in: query + name: image_url + required: false + type: string + - $ref: ../parameters.json#/match + - $ref: ../parameters.json#/order_by + - $ref: ../parameters.json#/page + - $ref: ../parameters.json#/per_page + - $ref: ../parameters.json#/q_param + - $ref: ../parameters.json#/q_body + - $ref: ../parameters.json#/q_header + produces: + - application/json + responses: + 200: + description: A list of authorised values + schema: + items: + $ref: ../definitions.json#/authorised_value + type: array + 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 + tags: + - authorised + - values + x-koha-authorization: + permissions: + catalogue: 1 + x-mojo-to: AuthorisedValues#list + post: + operationId: addAuthorisedValue + parameters: + - description: A JSON object containing informations about the new authorised value + in: body + name: body + required: true + schema: + $ref: ../definitions.json#/authorised_value + produces: + - application/json + responses: + 201: + description: Authorised value added + schema: + $ref: ../definitions.json#/authorised_value + 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 + tags: + - authorised + - values + x-koha-authorization: + permissions: + parameters: manage_auth_values + x-mojo-to: AuthorisedValues#add +"/authorised_values/{authorised_value_id}": + delete: + operationId: deleteAuthorisedValue + parameters: + - $ref: ../parameters.json#/authorised_value_id_pp + produces: + - application/json + responses: + 204: + description: Authorised value deleted + 401: + description: Authentication required + schema: + $ref: ../definitions.json#/error + 403: + description: Access forbidden + schema: + $ref: ../definitions.json#/error + 404: + description: Authorised value 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 + tags: + - authorised + - values + x-koha-authorization: + permissions: + parameters: manage_auth_values + x-mojo-to: AuthorisedValues#delete + get: + operationId: getAuthorisedValue + parameters: + - $ref: ../parameters.json#/authorised_value_id_pp + produces: + - application/json + responses: + 200: + description: An authorised value + schema: + $ref: ../definitions.json#/authorised_value + 404: + description: Authorised value 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 + tags: + - authorised + - values + x-koha-authorization: + permissions: + catalogue: 1 + x-mojo-to: AuthorisedValues#get + put: + operationId: updateAuthorisedValue + parameters: + - $ref: ../parameters.json#/authorised_value_id_pp + - description: An authorised value object + in: body + name: body + required: true + schema: + $ref: ../definitions.json#/authorised_value + produces: + - application/json + responses: + 200: + description: An authorised value + schema: + $ref: ../definitions.json#/authorised_value + 401: + description: Authentication required + schema: + $ref: ../definitions.json#/error + 403: + description: Access forbidden + schema: + $ref: ../definitions.json#/error + 404: + description: Authorised value 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 + tags: + - authorised + - value + x-koha-authorization: + permissions: + parameters: manage_auth_values + x-mojo-to: AuthorisedValues#update "/authorised_value_categories/{authorised_value_category_name}/authorised_values": get: x-mojo-to: AuthorisedValues#list_av_from_category @@ -66,7 +280,7 @@ schema: $ref: "../swagger.yaml#/definitions/error" 404: - description: Ressource not found + description: Resource not found schema: $ref: "../swagger.yaml#/definitions/error" 500: diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 227921d7957..2b372b868d7 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -217,6 +217,10 @@ paths: $ref: ./paths/auth.yaml#/~1auth~1identity_providers~1{identity_provider_id}~1domains "/auth/identity_providers/{identity_provider_id}/domains/{identity_provider_domain_id}": $ref: ./paths/auth.yaml#/~1auth~1identity_providers~1{identity_provider_id}~1domains~1{identity_provider_domain_id} + "/authorised_values": + $ref: ./paths/authorised_values.yaml#/~1authorised_values + "/authorised_values/{authorised_value_id}": + $ref: ./paths/authorised_values.yaml#/~1authorised_values~1{authorised_value_id} /authorised_value_categories: $ref: ./paths/authorised_value_categories.yaml#/~1authorised_value_categories "/authorised_value_categories/{authorised_value_category_name}/authorised_values": diff --git a/t/db_dependent/api/v1/authorised_values.t b/t/db_dependent/api/v1/authorised_values.t index 187087d4408..87c9a75383f 100755 --- a/t/db_dependent/api/v1/authorised_values.t +++ b/t/db_dependent/api/v1/authorised_values.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 1; +use Test::More tests => 6; use Test::Mojo; use t::lib::TestBuilder; @@ -85,3 +85,353 @@ subtest 'list_av_from_category() tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'list() tests' => sub { + + plan tests => 20; + + $schema->storage->txn_begin; + + Koha::AuthorisedValues->search->delete; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2 ** 2 } # catalogue flag = 2 + } + ); + 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 AVs, so empty array should be returned + $t->get_ok("//$userid:$password@/api/v1/authorised_values") + ->status_is(200) + ->json_is( [] ); + + my $av = $builder->build_object({ class => 'Koha::AuthorisedValues' }); + + # One av created, should get returned + $t->get_ok("//$userid:$password@/api/v1/authorised_values") + ->status_is(200) + ->json_is( [$av->to_api] ); + + my $another_av = $builder->build_object( + { class => 'Koha::AuthorisedValues', value => { lib => $av->lib } } ); + my $av_with_another_lib = $builder->build_object({ class => 'Koha::AuthorisedValues' }); + + # Two avs created, they should both be returned + $t->get_ok("//$userid:$password@/api/v1/authorised_values") + ->status_is(200) + ->json_is([$av->to_api, + $another_av->to_api, + $av_with_another_lib->to_api + ] ); + + # Filtering works, two avs sharing lib + $t->get_ok("//$userid:$password@/api/v1/authorised_values?description=" . $av->lib ) + ->status_is(200) + ->json_is([ $av->to_api, + $another_av->to_api + ]); + + $t->get_ok("//$userid:$password@/api/v1/authorised_values?value=" . $av->authorised_value ) + ->status_is(200) + ->json_is( [$av->to_api] ); + + # Warn on unsupported query parameter + $t->get_ok("//$userid:$password@/api/v1/authorised_values?av_blah=blah" ) + ->status_is(400) + ->json_is( [{ path => '/query/av_blah', message => 'Malformed query string'}] ); + + # Unauthorized access + $t->get_ok("//$unauth_userid:$password@/api/v1/authorised_values") + ->status_is(403); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $av = $builder->build_object({ class => 'Koha::AuthorisedValues' }); + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**2 } # catalogue flag = 2 + } + ); + 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/authorised_values/" . $av->id ) + ->status_is(200) + ->json_is($av->to_api); + + $t->get_ok( "//$unauth_userid:$password@/api/v1/authorised_values/" . $av->id ) + ->status_is(403); + + my $av_to_delete = $builder->build_object({ class => 'Koha::AuthorisedValues' }); + my $non_existent_id = $av_to_delete->id; + $av_to_delete->delete; + + $t->get_ok( "//$userid:$password@/api/v1/authorised_values/$non_existent_id" ) + ->status_is(404) + ->json_is( '/error' => 'Authorised value not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 18; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**3 } # parameters flag = 2 + } + ); + 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 $av_cat = $builder->build_object({ class => 'Koha::AuthorisedValueCategories' }); + my $av = { + category => $av_cat->category_name, + value => "a value", + description => "a lib", + opac_description => "opac lib" + }; + + # Unauthorized attempt to write + $t->post_ok("//$unauth_userid:$password@/api/v1/authorised_values" => json => $av) + ->status_is(403); + + # Authorized attempt to write invalid data + my $av_with_invalid_field = { + blah => "Av Blah", + category => $av_cat->category_name, + value => "a value", + description => "a lib", + opac_description => "opac lib" + }; + + $t->post_ok( "//$userid:$password@/api/v1/authorised_values" => json => $av_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + # Authorized attempt to write + my $av_id = + $t->post_ok( "//$userid:$password@/api/v1/authorised_values" => json => $av ) + ->status_is( 201, 'SWAGGER3.2.1' ) + ->header_like( + Location => qr|^\/api\/v1\/authorised_values/\d*|, + 'SWAGGER3.4.1' + ) + ->json_is( '/category' => $av->{category} ) + ->json_is( '/value' => $av->{value} ) + ->json_is( '/description' => $av->{description} ) + ->json_is( '/opac_description' => $av->{opac_description} ) + ->tx->res->json->{authorised_value_id}; + + # Authorized attempt to create with null id + $av->{authorised_value_id} = undef; + $t->post_ok( "//$userid:$password@/api/v1/authorised_values" => json => $av ) + ->status_is(400) + ->json_has('/errors'); + + # Authorized attempt to create with existing id + $av->{authorised_value_id} = $av_id; + $t->post_ok( "//$userid:$password@/api/v1/authorised_values" => json => $av ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Read-only.", + path => "/body/authorised_value_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 => 2**3 } # parameters flag = 2 + } + ); + 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 $av_id = $builder->build_object({ class => 'Koha::AuthorisedValues' } )->id; + + # Unauthorized attempt to update + $t->put_ok( "//$unauth_userid:$password@/api/v1/authorised_values/$av_id" => json => { name => 'New unauthorized name change' } ) + ->status_is(403); + + my $av_cat = $builder->build_object({ class => 'Koha::AuthorisedValueCategories' }); + + # Attempt partial update on a PUT + my $av_with_missing_field = { + category => $av_cat->category_name, + value => "a value", + }; + + $t->put_ok( "//$userid:$password@/api/v1/authorised_values/$av_id" => json => $av_with_missing_field ) + ->status_is(400) + ->json_is( "/errors" => + [ { message => "Missing property.", path => "/body/description" } ] + ); + + # Full object update on PUT + my $av_with_updated_field = { + category => $av_cat->category_name, + value => "a value", + description => "a lib", + }; + + $t->put_ok( "//$userid:$password@/api/v1/authorised_values/$av_id" => json => $av_with_updated_field ) + ->status_is(200) + ->json_is( '/description' => 'a lib' ); + + # Authorized attempt to write invalid data + my $av_with_invalid_field = { + blah => "Av Blah", + category => $av_cat->category_name, + value => "a value", + description => "a lib", + opac_description => "opac lib" + }; + + $t->put_ok( "//$userid:$password@/api/v1/authorised_values/$av_id" => json => $av_with_invalid_field ) + ->status_is(400) + ->json_is( + "/errors" => [ + { + message => "Properties not allowed: blah.", + path => "/body" + } + ] + ); + + my $av_to_delete = $builder->build_object({ class => 'Koha::AuthorisedValues' }); + my $non_existent_id = $av_to_delete->id; + $av_to_delete->delete; + + $t->put_ok( "//$userid:$password@/api/v1/authorised_values/$non_existent_id" => json => $av_with_updated_field ) + ->status_is(404); + + # Wrong method (POST) + $av_with_updated_field->{authorised_value_id} = 2; + + $t->post_ok( "//$userid:$password@/api/v1/authorised_values/$av_id" => json => $av_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 => 2**3 } # parameters flag = 2 + } + ); + 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 $av_id = $builder->build_object({ class => 'Koha::AuthorisedValues' })->id; + + # Unauthorized attempt to delete + $t->delete_ok( "//$unauth_userid:$password@/api/v1/authorised_values/$av_id" ) + ->status_is(403); + + $t->delete_ok("//$userid:$password@/api/v1/authorised_values/$av_id") + ->status_is(204, 'SWAGGER3.2.4') + ->content_is('', 'SWAGGER3.3.4'); + + $t->delete_ok("//$userid:$password@/api/v1/authorised_values/$av_id") + ->status_is(404); + + $schema->storage->txn_rollback; +}; -- 2.43.0