From 2a8631847ed44a07de3cff8392bef66e79aab576 Mon Sep 17 00:00:00 2001 From: Nick Clemens Date: Tue, 19 Oct 2021 19:34:47 +0000 Subject: [PATCH] Bug 22785: API files --- Koha/REST/V1/ImportRecordMatches.pm | 106 +++++++++++ .../definitions/import_record_match.json | 21 +++ .../parameters/import_record_match.json | 16 ++ .../swagger/paths/import_record_matches.json | 144 +++++++++++++++ t/db_dependent/api/v1/import_record_matches.t | 167 ++++++++++++++++++ 5 files changed, 454 insertions(+) create mode 100644 Koha/REST/V1/ImportRecordMatches.pm create mode 100644 api/v1/swagger/definitions/import_record_match.json create mode 100644 api/v1/swagger/parameters/import_record_match.json create mode 100644 api/v1/swagger/paths/import_record_matches.json create mode 100755 t/db_dependent/api/v1/import_record_matches.t diff --git a/Koha/REST/V1/ImportRecordMatches.pm b/Koha/REST/V1/ImportRecordMatches.pm new file mode 100644 index 0000000000..33de3062f9 --- /dev/null +++ b/Koha/REST/V1/ImportRecordMatches.pm @@ -0,0 +1,106 @@ +package Koha::REST::V1::ImportRecordMatches; + +# 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::Import::Record::Matches; + +use Try::Tiny; + +=head1 API + +=head2 Methods + +=cut + +=head3 unset_chosen + +Method that handles unselecting all chosen matches for an import record + +DELETE /api/v1/import/{import_batch_id}/records/{import_record_id}/matches/chosen + +=cut + +sub unset_chosen { + my $c = shift->openapi->valid_input or return; + + my $import_record_id = $c->validation->param('import_record_id'); + my $matches = Koha::Import::Record::Matches->search({ + import_record_id => $import_record_id, + }); + unless ($matches) { + return $c->render( + status => 404, + openapi => { error => "No matches not found" } + ); + } + return try { + $matches->unset_chosen; + return $c->render( status => 204, openapi => $matches ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 set_chosen + +Method that handles modifying if a Koha::Import::Record::Match object has been chosen for overlay + +PUT /api/v1/import/{import_batch_id}/records/{import_record_id}/matches/chosen + +Body should contain the condidate_match_id to chose + +=cut + +sub set_chosen { + my $c = shift->openapi->valid_input or return; + + my $import_record_id = $c->validation->param('import_record_id'); + my $body = $c->validation->param('body'); + my $candidate_match_id = $body->{'candidate_match_id'}; + + my $match = Koha::Import::Record::Matches->find({ + import_record_id => $import_record_id, + candidate_match_id => $candidate_match_id + }); + + unless ($match) { + return $c->render( + status => 404, + openapi => { error => "Match not found" } + ); + } + + return try { + my $matches = Koha::Import::Record::Matches->search({ + import_record_id => $import_record_id, + chosen => 1 + }); + $matches->unset_chosen if $matches; + $match->set_from_api({ chosen => JSON::true }); + $match->store; + return $c->render( status => 200, openapi => $match ); + } + catch { + $c->unhandled_exception($_); + }; +} + +1; diff --git a/api/v1/swagger/definitions/import_record_match.json b/api/v1/swagger/definitions/import_record_match.json new file mode 100644 index 0000000000..abde7365b0 --- /dev/null +++ b/api/v1/swagger/definitions/import_record_match.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "properties": { + "import_record_id": { + "type": "integer", + "description": "Internal import record identifier" + }, + "candidate_match_id": { + "type": "integer", + "description": "Internal import record match candidate identifier" + }, + "chosen": { + "type": "boolean", + "description": "Whether match has been chosen for overlay" + }, + "score": { + "type": "integer", + "description": "Ranking value for this match calculated by the matching rules" + } + } +} diff --git a/api/v1/swagger/parameters/import_record_match.json b/api/v1/swagger/parameters/import_record_match.json new file mode 100644 index 0000000000..5268c2a2dc --- /dev/null +++ b/api/v1/swagger/parameters/import_record_match.json @@ -0,0 +1,16 @@ +{ + "import_record_id_pp": { + "name": "import_record_id", + "in": "path", + "description": "Internal import_record identifier", + "required": true, + "type": "integer" + }, + "candidate_match_id_pp": { + "name": "candidate_match_id", + "in": "path", + "description": "Internal import_record_match_candidate identifier", + "required": true, + "type": "integer" + } +} diff --git a/api/v1/swagger/paths/import_record_matches.json b/api/v1/swagger/paths/import_record_matches.json new file mode 100644 index 0000000000..3c0be19d97 --- /dev/null +++ b/api/v1/swagger/paths/import_record_matches.json @@ -0,0 +1,144 @@ +{ + "/import/{import_batch_id}/records/{import_record_id}/matches/chosen": { + "put": { + "x-mojo-to": "ImportRecordMatches#set_chosen", + "operationId": "setChosen", + "tags": ["import_record_matches"], + "parameters": [{ + "name": "import_batch_id", + "in": "path", + "required": true, + "description": "An import_batch ID", + "type": "integer" + }, { + "name": "import_record_id", + "in": "path", + "required": true, + "description": "An import_record ID", + "type": "integer" + }, { + "name": "body", + "in": "body", + "description": "A JSON object containing fields to modify", + "required": true, + "schema": { + "type": "object", + "properties": { + "candidate_match_id": { + "description": "Candudate match to choose", + "type": "integer" + } + } + } + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Match updated" + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Match management not allowed", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Import record match not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "tools": "manage_staged_marc" + } + } + }, + "delete": { + "x-mojo-to": "ImportRecordMatches#unset_chosen", + "operationId": "unsetChosen", + "tags": ["import_record_matches"], + "parameters": [{ + "name": "import_batch_id", + "in": "path", + "required": true, + "description": "An import_batch ID", + "type": "integer" + }, { + "name": "import_record_id", + "in": "path", + "required": true, + "description": "An import_record ID", + "type": "integer" + }], + "produces": ["application/json"], + "responses": { + "204": { + "description": "Matches unchosen" + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Match management not allowed", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Import record matches not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "tools": "manage_staged_marc" + } + } + } + } +} diff --git a/t/db_dependent/api/v1/import_record_matches.t b/t/db_dependent/api/v1/import_record_matches.t new file mode 100755 index 0000000000..9e79e4a85c --- /dev/null +++ b/t/db_dependent/api/v1/import_record_matches.t @@ -0,0 +1,167 @@ +#!/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, see . + +use Modern::Perl; + +use Test::More tests => 1; +use Test::Mojo; +use Test::Warn; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +use C4::Auth; +use Koha::Import::Record::Matches; + +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 'import record matches 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 $match_1 = $builder->build_object({ + class => 'Koha::Import::Record::Matches', + value => { + chosen => 0, + } + }); + my $match_2 = $builder->build_object({ + class => 'Koha::Import::Record::Matches', + value => { + import_record_id => $match_1->import_record_id, + chosen => 1, + } + }); + my $del_match = $builder->build_object({ class => 'Koha::Import::Record::Matches' }); + my $del_import_batch_id = $del_match->import_record->import_batch_id; + my $del_match_id = $del_match->import_record_id; + + # Unauthorized attempt to update + my $tx = $t->ua->build_tx( + PUT => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen"=> + json => { + candidate_match_id => $match_1->candidate_match_id + } + ); + $tx->req->cookies( + { name => 'CGISESSID', value => $unauthorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(403); + + # Invalid attempt to allow match on a non-existent record + $tx = $t->ua->build_tx( + PUT => "/api/v1/import/".$del_import_batch_id."/records/".$del_match_id."/matches/chosen" => + json => { + candidate_match_id => $match_1->candidate_match_id + } + ); + + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $del_match->delete(); + $t->request_ok($tx)->status_is(404) + ->json_is( '/error' => "Match not found" ); + + # Valid, authorised update + $tx = $t->ua->build_tx( + PUT => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen" => + json => { + candidate_match_id => $match_1->candidate_match_id + } + ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(200); + + $match_1->discard_changes; + $match_2->discard_changes; + ok( $match_1->chosen,"Match 1 is correctly set to chosen"); + ok( !$match_2->chosen,"Match 2 correctly unset when match 1 is set"); + + # Valid unsetting + $tx = $t->ua->build_tx( + DELETE => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen" => + json => { + } + ); + $tx->req->cookies( + { name => 'CGISESSID', value => $authorized_session_id } ); + $tx->req->env( { REMOTE_ADDR => $remote_address } ); + $t->request_ok($tx)->status_is(204); + + $match_1->discard_changes; + $match_2->discard_changes; + ok( !$match_1->chosen,"Match 1 is correctly unset to chosen"); + ok( !$match_2->chosen,"Match 2 is correctly unset to chosen"); + + $schema->storage->txn_rollback; +}; + +sub create_user_and_session { + + my $args = shift; + my $dbh = C4::Context->dbh; + + my $user = $builder->build( + { + source => 'Borrower', + value => { + flags => 0 + } + } + ); + + # 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} ) { + $builder->build({ + source => 'UserPermission', + value => { + borrowernumber => $user->{borrowernumber}, + module_bit => 13, + code => 'manage_staged_marc', + } + }); + } + + return ( $user->{borrowernumber}, $session->id ); +} + +1; -- 2.20.1