From cde6ca3b46bda6b74cddd4daa9cd7db43bc17ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Moyano?= Date: Fri, 15 Sep 2023 09:24:11 -0300 Subject: [PATCH] Bug 32607: Add /record_sources endpoints This patch introduces endpoints for managing record sources. This is done on top of Koha::RecordSource(s) following the current coding style. To test: 1. Apply this patch 2. Run: $ ktd --shell k$ prove t/db_dependent/api/v1/record_sources.t => SUCCESS: Tests pass! 3. Sign off :-D Signed-off-by: Tomas Cohen Arazi --- Koha/REST/V1/RecordSources.pm | 141 ++++++++ api/v1/swagger/definitions/record_source.yaml | 26 ++ api/v1/swagger/paths/record_sources.yaml | 256 +++++++++++++++ api/v1/swagger/swagger.yaml | 15 + t/db_dependent/api/v1/record_sources.t | 303 ++++++++++++++++++ 5 files changed, 741 insertions(+) create mode 100644 Koha/REST/V1/RecordSources.pm create mode 100644 api/v1/swagger/definitions/record_source.yaml create mode 100644 api/v1/swagger/paths/record_sources.yaml create mode 100755 t/db_dependent/api/v1/record_sources.t diff --git a/Koha/REST/V1/RecordSources.pm b/Koha/REST/V1/RecordSources.pm new file mode 100644 index 00000000000..34afb322503 --- /dev/null +++ b/Koha/REST/V1/RecordSources.pm @@ -0,0 +1,141 @@ +package Koha::REST::V1::RecordSources; + +# Copyright 2023 Theke Solutions +# +# 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, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::RecordSources; + +use Try::Tiny qw( catch try ); + +=head1 API + +=head2 Methods + +=head3 list + +=cut + +sub list { + my $c = shift->openapi->valid_input or return; + + return try { + my $sources = $c->objects->search( Koha::RecordSources->new ); + return $c->render( status => 200, openapi => $sources ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 get + +=cut + +sub get { + my $c = shift->openapi->valid_input or return; + + return try { + my $source = $c->objects->find( Koha::RecordSources->new, $c->param('record_source_id') ); + unless ($source) { + return $c->render( + status => 404, + openapi => { error => "Object not found" } + ); + } + + return $c->render( status => 200, openapi => $source ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 add + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + return try { + my $source = Koha::RecordSource->new_from_api( $c->req->json ); + $source->store; + $c->res->headers->location( $c->req->url->to_string . '/' . $source->record_source_id ); + return $c->render( + status => 201, + openapi => $c->objects->to_api($source) + ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 update + +=cut + +sub update { + my $c = shift->openapi->valid_input or return; + + my $source = $c->objects->find_rs( Koha::RecordSources->new, $c->param('record_source_id') ); + + unless ($source) { + return $c->render( + status => 404, + openapi => { error => "Object not found" } + ); + } + + return try { + $source->set_from_api( $c->req->json )->store; + $source->discard_changes; + return $c->render( status => 200, openapi => $c->objects->to_api($source) ); + } catch { + $c->unhandled_exception($_); + }; +} + +=head3 delete + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $source = $c->objects->find_rs( Koha::RecordSources->new, $c->param('record_source_id') ); + + unless ($source) { + return $c->render( + status => 404, + openapi => { error => "Object not found" } + ); + } + + return try { + $source->delete; + return $c->render( + status => 204, + openapi => q{} + ); + } catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/api/v1/swagger/definitions/record_source.yaml b/api/v1/swagger/definitions/record_source.yaml new file mode 100644 index 00000000000..50be12e20f2 --- /dev/null +++ b/api/v1/swagger/definitions/record_source.yaml @@ -0,0 +1,26 @@ +--- +type: object +properties: + record_source_id: + type: integer + description: internally assigned record source identifier + readOnly: true + name: + description: record source name + type: string + patron_id: + description: linked patron identifier + type: integer + api_token: + description: A token for identifying record sources on API interactions + type: string + readOnly: true + patron: + type: + - object + - "null" + description: Related patron object (x-koha-embed) +additionalProperties: false +required: + - name + - patron_id diff --git a/api/v1/swagger/paths/record_sources.yaml b/api/v1/swagger/paths/record_sources.yaml new file mode 100644 index 00000000000..af8a7205023 --- /dev/null +++ b/api/v1/swagger/paths/record_sources.yaml @@ -0,0 +1,256 @@ +/record_sources: + get: + x-mojo-to: RecordSources#list + operationId: listRecordSources + summary: List record sources + tags: + - record_sources + parameters: + - $ref: "../swagger.yaml#/parameters/match" + - $ref: "../swagger.yaml#/parameters/order_by" + - $ref: "../swagger.yaml#/parameters/page" + - $ref: "../swagger.yaml#/parameters/per_page" + - $ref: "../swagger.yaml#/parameters/q_param" + - $ref: "../swagger.yaml#/parameters/q_body" + - $ref: "../swagger.yaml#/parameters/request_id_header" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - patron + collectionFormat: csv + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: A list of record sources + schema: + type: array + items: + $ref: "../swagger.yaml#/definitions/record_source" + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_record_sources + post: + x-mojo-to: RecordSources#add + operationId: addRecordSources + summary: Add a record source + tags: + - record_sources + parameters: + - name: body + in: body + description: A JSON object containing informations about the new record source + required: true + schema: + $ref: "../swagger.yaml#/definitions/record_source" + consumes: + - application/json + produces: + - application/json + responses: + "201": + description: A record source + schema: + $ref: "../swagger.yaml#/definitions/record_source" + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_record_sources +"/record_sources/{record_source_id}": + get: + x-mojo-to: RecordSources#get + operationId: getRecordSources + summary: Get a record source + tags: + - record_sources + parameters: + - $ref: "../swagger.yaml#/parameters/record_source_id_pp" + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: A record source + schema: + $ref: "../swagger.yaml#/definitions/record_source" + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_record_sources + put: + x-mojo-to: RecordSources#update + operationId: updateRecordSources + summary: Update a record source + tags: + - record_sources + parameters: + - $ref: "../swagger.yaml#/parameters/record_source_id_pp" + - name: body + in: body + description: A JSON object containing informations about the new record source + required: true + schema: + $ref: "../swagger.yaml#/definitions/record_source" + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: A record source + schema: + $ref: "../swagger.yaml#/definitions/record_source" + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_record_sources + delete: + x-mojo-to: RecordSources#delete + operationId: deleteRecordSources + summary: Delete a record source + tags: + - record_sources + parameters: + - $ref: "../swagger.yaml#/parameters/record_source_id_pp" + consumes: + - application/json + produces: + - application/json + responses: + "204": + description: Deleted + "400": + description: Missing or wrong parameters + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Not allowed + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + parameters: manage_record_sources diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index c52d03462ac..50a7c5d51ee 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -70,6 +70,8 @@ definitions: $ref: ./definitions/import_batch_profiles.yaml import_record_match: $ref: ./definitions/import_record_match.yaml + record_source: + $ref: ./definitions/record_source.yaml invoice: $ref: ./definitions/invoice.yaml item: @@ -275,6 +277,10 @@ paths: $ref: ./paths/import_batch_profiles.yaml#/~1import_batch_profiles "/import_batch_profiles/{import_batch_profile_id}": $ref: "./paths/import_batch_profiles.yaml#/~1import_batch_profiles~1{import_batch_profile_id}" + /record_sources: + $ref: ./paths/record_sources.yaml#/~1record_sources + "/record_sources/{record_source_id}": + $ref: ./paths/record_sources.yaml#/~1record_sources~1{record_source_id} /items: $ref: ./paths/items.yaml#/~1items "/items/{item_id}": @@ -533,6 +539,12 @@ parameters: name: import_record_id required: true type: integer + record_source_id_pp: + description: Internal record source identifier + in: path + name: record_source_id + required: true + type: integer item_id_pp: description: Internal item identifier in: path @@ -860,6 +872,9 @@ tags: - description: "Manage item groups\n" name: item_groups x-displayName: Item groups + - description: "Manage record sources\n" + name: record_sources + x-displayName: Record source - description: "Manage items\n" name: items x-displayName: Items diff --git a/t/db_dependent/api/v1/record_sources.t b/t/db_dependent/api/v1/record_sources.t new file mode 100755 index 00000000000..ef440262c1e --- /dev/null +++ b/t/db_dependent/api/v1/record_sources.t @@ -0,0 +1,303 @@ +#!/usr/bin/env perl + +# Copyright 2023 Theke Solutions +# +# 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, see . + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Mojo; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use Koha::RecordSources; +use Koha::Database; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); + +my $t = Test::Mojo->new('Koha::REST::V1'); + +subtest 'list() tests' => sub { + + plan tests => 12; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( { class => 'Koha::RecordSources' } ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + for ( 1 .. 10 ) { + $builder->build_object( { class => 'Koha::RecordSources' } ); + } + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + $t->get_ok("//$userid:$password@/api/v1/record_sources")->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->get_ok("//$userid:$password@/api/v1/record_sources?_per_page=10")->status_is( 200, 'SWAGGER3.2.2' ); + + my $response_count = scalar @{ $t->tx->res->json }; + + is( $response_count, 10, 'The API returns 10 sources' ); + + my $id = $source->record_source_id; + $t->get_ok("//$userid:$password@/api/v1/record_sources?q={\"record_source_id\": $id}")->status_is(200) + ->json_is( '' => [ $source->to_api ], 'SWAGGER3.3.2' ); + + $source->delete; + + $t->get_ok("//$userid:$password@/api/v1/record_sources?q={\"record_source_id\": $id}")->status_is(200) + ->json_is( '' => [] ); + + $schema->storage->txn_rollback; +}; + +subtest 'get() tests' => sub { + + plan tests => 9; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( { class => 'Koha::RecordSources' } ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + my $id = $source->record_source_id; + + $t->get_ok("//$userid:$password@/api/v1/record_sources/$id")->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->get_ok("//$userid:$password@/api/v1/record_sources/$id")->status_is( 200, 'SWAGGER3.2.2' ) + ->json_is( '' => $source->to_api, 'SWAGGER3.3.2' ); + + $source->delete; + + $t->get_ok("//$userid:$password@/api/v1/record_sources/$id")->status_is(404) + ->json_is( '/error' => 'Object not found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'delete() tests' => sub { + + plan tests => 6; + + $schema->storage->txn_begin; + + my $source = $builder->build_object( { class => 'Koha::RecordSources' } ); + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + + my $id = $source->record_source_id; + + $t->delete_ok("//$userid:$password@/api/v1/record_sources/$id")->status_is(403) + ->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + $t->delete_ok("//$userid:$password@/api/v1/record_sources/$id")->status_is( 204, 'SWAGGER3.2.2' ); + + my $deleted_source = Koha::RecordSources->search( { record_source_id => $id } ); + + is( $deleted_source->count, 0, 'No record source found' ); + + $schema->storage->txn_rollback; +}; + +subtest 'add() tests' => sub { + + plan tests => 8; + + $schema->storage->txn_begin; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 3 } + } + ); + + my $nonprivilegedpatron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + + my $password = 'thePassword123'; + + $nonprivilegedpatron->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $nonprivilegedpatron->userid; + my $patron_id = $nonprivilegedpatron->borrowernumber; + + $t->post_ok( "//$userid:$password@/api/v1/record_sources" => json => { name => 'test1', patron_id => $patron_id } ) + ->status_is(403)->json_is( '/error' => 'Authorization failure. Missing required permission(s).' ); + + $patron->set_password( { password => $password, skip_validation => 1 } ); + $userid = $patron->userid; + + my $source_id = $t->post_ok( + "//$userid:$password@/api/v1/record_sources" => json => { name => 'test1', patron_id => $patron_id } ) + ->status_is( 201, 'SWAGGER3.2.2' )->json_is( '/name', 'test1' )->json_is( '/patron_id', $patron_id ) + ->tx->res->json->{record_source_id}; + + my $created_source = Koha::RecordSources->find($source_id); + + is( $created_source->name, 'test1', 'Record source found' ); + + $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 $source = Koha::RecordSource->new( { name => 'old_name', patron_id => $patron->id } )->store; + my $source_id = $source->id; + + # Unauthorized attempt to update + $t->put_ok( "//$unauth_userid:$password@/api/v1/record_sources/$source_id" => json => + { name => 'New unauthorized name change' } )->status_is(403); + + # Attempt partial update on a PUT + my $source_with_missing_field = { patron_id => $source->patron_id }; + + $t->put_ok( "//$userid:$password@/api/v1/record_sources/$source_id" => json => $source_with_missing_field ) + ->status_is(400)->json_is( "/errors" => [ { message => "Missing property.", path => "/body/name" } ] ); + + # Full object update on PUT + my $source_with_updated_field = { + name => "new_name", + patron_id => $source->patron_id, + }; + + $t->put_ok( "//$userid:$password@/api/v1/record_sources/$source_id" => json => $source_with_updated_field ) + ->status_is(200)->json_is( '/name' => $source_with_updated_field->{name} ); + + # Authorized attempt to write invalid data + my $source_with_invalid_field = { + name => "blah", + patron_id => $source->patron_id, + potato => "yeah", + }; + + $t->put_ok( "//$userid:$password@/api/v1/record_sources/$source_id" => json => $source_with_invalid_field ) + ->status_is(400)->json_is( + "/errors" => [ + { + message => "Properties not allowed: potato.", + path => "/body" + } + ] + ); + + my $source_to_delete = $builder->build_object( { class => 'Koha::RecordSources' } ); + my $non_existent_id = $source_to_delete->id; + $source_to_delete->delete; + + $t->put_ok( "//$userid:$password@/api/v1/record_sources/$non_existent_id" => json => $source_with_updated_field ) + ->status_is(404); + + # Wrong method (POST) + $source_with_updated_field->{record_source_id} = 2; + + $t->post_ok( "//$userid:$password@/api/v1/record_sources/$source_id" => json => $source_with_updated_field ) + ->status_is(404); + + $schema->storage->txn_rollback; +}; -- 2.42.0