Bug 37762: Send message back to requesting agency
This commit applies changes to V1.pm to be able to utilize the same XML methods. We're also updating tests to reflect this new functionality. Signed-off-by: Jason Robb <jrobb@sekls.org> Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
ae3b70b66b
commit
f036c92ec0
|
|
@ -24,6 +24,7 @@ use Koha::REST::V1;
|
|||
use XML::LibXML;
|
||||
use JSON qw( encode_json decode_json );
|
||||
use File::Basename qw( dirname );
|
||||
use LWP::UserAgent;
|
||||
|
||||
use Koha::Biblios;
|
||||
use Koha::ILL::ISO18626::RequestingAgency;
|
||||
|
|
@ -103,43 +104,108 @@ sub messages {
|
|||
|
||||
=head3 send_message
|
||||
|
||||
Send a message to the requesting agency
|
||||
# $message must be a valid XML string.
|
||||
Send an ISO18626 message payload to the requesting agency's callback endpoint.
|
||||
Expects C<$message> as a Perl hashref, which is converted to XML before sending.
|
||||
|
||||
=cut
|
||||
|
||||
sub send_message {
|
||||
my ( $self, $type, $message ) = @_;
|
||||
|
||||
# my $requesting_agency = $self->_result->requesting_agency;
|
||||
|
||||
# return unless $requesting_agency->callback_endpoint;
|
||||
|
||||
# my $ua = LWP::UserAgent->new( agent => 'Koha ILL' );
|
||||
# $ua->agent( 'Koha/' . Koha::version() );
|
||||
|
||||
# my $response = $ua->post(
|
||||
# $requesting_agency->callback_endpoint,
|
||||
# Content_Type => 'application/xml',
|
||||
# Content => $message,
|
||||
# );
|
||||
|
||||
# if ( $response->is_success ) {
|
||||
#FIXME: Better way to convert this to json other than invoking Koha::REST::V1 ?
|
||||
# my $parser = XML::LibXML->new();
|
||||
# my $doc = $parser->parse_string($message);
|
||||
# my $root = $doc->documentElement();
|
||||
# my $json = Koha::REST::V1::parse_xml($root);
|
||||
|
||||
# $json = JSON::encode_json($json);
|
||||
$self->add_message( { type => $type, message => $message } );
|
||||
return 1;
|
||||
|
||||
# }
|
||||
# else {
|
||||
# $self->_result->add_message( 'Request', "Failed to send message: " . $response->status_line );
|
||||
# return;
|
||||
# }
|
||||
my $requesting_agency = $self->requesting_agency;
|
||||
if ( !$requesting_agency ) {
|
||||
warn sprintf(
|
||||
"ISO18626: Cannot send message. No requesting agency linked to request %s",
|
||||
$self->iso18626_request_id
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( !$requesting_agency->callback_endpoint ) {
|
||||
warn sprintf(
|
||||
"ISO18626: Cannot send message. Requesting agency %s has no callback_endpoint defined.",
|
||||
$requesting_agency->id
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
my $xml_payload;
|
||||
eval { $xml_payload = Koha::REST::V1::to_xml($message); };
|
||||
if ($@) {
|
||||
warn sprintf( "ISO18626: Failed to convert message to XML for request %s: %s", $self->iso18626_request_id, $@ );
|
||||
return 0;
|
||||
}
|
||||
|
||||
my $ua = LWP::UserAgent->new(
|
||||
agent => 'Koha ' . $Koha::VERSION,
|
||||
timeout => 10,
|
||||
);
|
||||
|
||||
my $response = $ua->post(
|
||||
$requesting_agency->callback_endpoint,
|
||||
'Content-Type' => 'application/xml',
|
||||
Content => $xml_payload,
|
||||
);
|
||||
|
||||
if ( $response->is_success ) {
|
||||
my $content = $response->decoded_content;
|
||||
|
||||
if ($content) {
|
||||
my $parser = XML::LibXML->new();
|
||||
my $doc;
|
||||
eval { $doc = $parser->parse_string($content); };
|
||||
|
||||
if ( $@ || !$doc ) {
|
||||
warn sprintf(
|
||||
"ISO18626: Could not parse XML confirmation from %s for request %s. Error: %s",
|
||||
$requesting_agency->callback_endpoint,
|
||||
$self->iso18626_request_id,
|
||||
$@
|
||||
);
|
||||
} else {
|
||||
my $root = $doc->documentElement();
|
||||
my $parsed_json;
|
||||
eval {
|
||||
my $spec_file = dirname(__FILE__) . "/../../../api/v1/swagger/swagger_bundle.json";
|
||||
if ( !-f $spec_file ) {
|
||||
$spec_file = dirname(__FILE__) . "/../../../api/v1/swagger/swagger.yaml";
|
||||
}
|
||||
$parsed_json = Koha::REST::V1::parse_xml( $root, $spec_file );
|
||||
};
|
||||
|
||||
if ($@) {
|
||||
warn sprintf(
|
||||
"ISO18626: Failed to convert XML to JSON for confirmation on request %s. Error: %s",
|
||||
$self->iso18626_request_id,
|
||||
$@
|
||||
);
|
||||
} else {
|
||||
my $confirmation_type = $type . 'Confirmation';
|
||||
$self->add_message( { type => $confirmation_type, message => $parsed_json } );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn sprintf(
|
||||
"ISO18626: HTTP request for %s on request %s was successful but returned an empty body.",
|
||||
$type,
|
||||
$self->iso18626_request_id
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
# If we reach this point, the HTTP request failed
|
||||
warn sprintf(
|
||||
"ISO18626: HTTP Request Failed. Could not send %s to %s. Status: %s. Body: %s",
|
||||
$type,
|
||||
$requesting_agency->callback_endpoint,
|
||||
$response->status_line,
|
||||
$response->decoded_content || 'No content provided'
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
=head3 requesting_agency
|
||||
|
|
|
|||
|
|
@ -76,10 +76,14 @@ sub startup {
|
|||
try {
|
||||
my $xml = $c->req->body;
|
||||
|
||||
my $parser = XML::LibXML->new();
|
||||
my $doc = $parser->parse_string($xml);
|
||||
my $root = $doc->documentElement();
|
||||
my $json = $self->parse_xml($root);
|
||||
my $parser = XML::LibXML->new();
|
||||
my $doc = $parser->parse_string($xml);
|
||||
my $root = $doc->documentElement();
|
||||
my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json");
|
||||
if ( !-f $spec_file ) {
|
||||
$spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml");
|
||||
}
|
||||
my $json = parse_xml( $root, $spec_file );
|
||||
|
||||
$c->req->body( JSON::encode_json($json) );
|
||||
|
||||
|
|
@ -330,38 +334,33 @@ sub validate_json_payload {
|
|||
|
||||
=head3 parse_xml
|
||||
|
||||
my $hashref = $self->parse_xml( $xml_node );
|
||||
my $hashref = parse_xml( $xml_node, $spec_file );
|
||||
|
||||
Converts an C<XML::LibXML::Node> into a Perl hashref, using the OpenAPI schema definitions to correctly identify and cast array properties.
|
||||
|
||||
=cut
|
||||
|
||||
sub parse_xml {
|
||||
my ( $self, $node ) = @_;
|
||||
my ( $node, $spec_file ) = @_;
|
||||
my $hash = {};
|
||||
|
||||
my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json");
|
||||
if ( !-f $spec_file ) {
|
||||
$spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml");
|
||||
}
|
||||
|
||||
my $schema = JSON::Validator::Schema::OpenAPIv2->new($spec_file);
|
||||
|
||||
$self->_parse_node( $node, $hash, 0, $schema->data->{definitions} );
|
||||
_parse_node( $node, $hash, 0, $schema->data->{definitions} );
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
=head3 _parse_node
|
||||
|
||||
$self->_parse_node( $node, $hash, $is_array, $schema_definitions );
|
||||
_parse_node( $node, $hash, $is_array, $schema_definitions );
|
||||
|
||||
Internal recursive helper for C<parse_xml> to populate a Perl hashref from XML elements.
|
||||
|
||||
=cut
|
||||
|
||||
sub _parse_node {
|
||||
my ( $self, $node, $hash, $is_array, $schema_definitions ) = @_;
|
||||
my ( $node, $hash, $is_array, $schema_definitions ) = @_;
|
||||
|
||||
my $name = $node->localName();
|
||||
my $properties = $schema_definitions->{$name}->{properties};
|
||||
|
|
@ -377,9 +376,9 @@ sub _parse_node {
|
|||
foreach my $child ( $node->childNodes() ) {
|
||||
if ( $child->nodeType() == 1 ) { # 1 is the node type for elements
|
||||
if ( grep { $_ eq $child->localName() } @array_properties ) {
|
||||
$self->_parse_node( $child, $child_hash, 1, $schema_definitions );
|
||||
_parse_node( $child, $child_hash, 1, $schema_definitions );
|
||||
} else {
|
||||
$self->_parse_node( $child, $child_hash, 0, $schema_definitions );
|
||||
_parse_node( $child, $child_hash, 0, $schema_definitions );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@
|
|||
use Modern::Perl;
|
||||
|
||||
use Test::NoWarnings;
|
||||
use Test::More tests => 2;
|
||||
use Test::More tests => 3;
|
||||
|
||||
use Test::MockModule;
|
||||
use Test::MockObject;
|
||||
use Test::Mojo;
|
||||
use Test::Warn;
|
||||
|
||||
use MIME::Base64 qw(encode_base64);
|
||||
use JSON qw(encode_json);
|
||||
|
|
@ -125,10 +126,27 @@ XML
|
|||
qr{<errorValue>AuthenticationFailed</errorValue>},
|
||||
'error value is authentication failed'
|
||||
);
|
||||
my $supplyingAgencyMessageConfirmationXml = <<'XML';
|
||||
<supplyingAgencyMessageConfirmation xmlns="https://example.com/ill/request">
|
||||
<confirmationHeader>
|
||||
<timestamp>2023-01-01T00:00:00Z</timestamp>
|
||||
</confirmationHeader>
|
||||
</supplyingAgencyMessageConfirmation>
|
||||
XML
|
||||
|
||||
my $mock_ua_response = Test::MockObject->new();
|
||||
$mock_ua_response->mock( 'is_success', sub { return 1; } );
|
||||
$mock_ua_response->mock( 'decoded_content', sub { return $supplyingAgencyMessageConfirmationXml; } );
|
||||
$mock_ua_response->mock( 'status_line', sub { return '200 OK'; } );
|
||||
|
||||
my $mock_ua = Test::MockModule->new('LWP::UserAgent');
|
||||
$mock_ua->mock( 'post', sub { return $mock_ua_response; } );
|
||||
|
||||
my $requesting_agency = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::ILL::ISO18626::RequestingAgencies',
|
||||
value => { account_id => 'asd', securityCode => 'asds' }
|
||||
value =>
|
||||
{ account_id => 'asd', securityCode => 'asds', callback_endpoint => 'http://localhost/ill/callback' }
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -249,3 +267,191 @@ XML
|
|||
$schema->storage->txn_rollback;
|
||||
};
|
||||
|
||||
subtest 'send_message() tests' => sub {
|
||||
|
||||
plan tests => 18;
|
||||
|
||||
$schema->storage->txn_begin;
|
||||
|
||||
my $librarian = $builder->build_object(
|
||||
{
|
||||
class => 'Koha::Patrons',
|
||||
value => { flags => 2**22 } # 22 => ill
|
||||
}
|
||||
);
|
||||
my $password = 'thePassword123';
|
||||
$librarian->set_password( { password => $password, skip_validation => 1 } );
|
||||
my $userid = $librarian->userid;
|
||||
|
||||
# Test: no callback_endpoint defined on requesting agency
|
||||
$builder->build_object(
|
||||
{
|
||||
class => 'Koha::ILL::ISO18626::RequestingAgencies',
|
||||
value => { account_id => 'no_callback_test', securityCode => 'test_secret_1', callback_endpoint => '' }
|
||||
}
|
||||
);
|
||||
|
||||
my $request_no_callback_xml = <<'XML';
|
||||
<request xmlns="https://example.com/ill/request">
|
||||
<header>
|
||||
<requestingAgencyAuthentication>
|
||||
<accountId>no_callback_test</accountId>
|
||||
<securityCode>test_secret_1</securityCode>
|
||||
</requestingAgencyAuthentication>
|
||||
<requestingAgencyRequestId>XYZ</requestingAgencyRequestId>
|
||||
<timestamp>2023-03-15 14:30:00</timestamp>
|
||||
<requestingAgencyId>
|
||||
<agencyIdType>ISIL</agencyIdType>
|
||||
<agencyIdValue>req_agency_value</agencyIdValue>
|
||||
</requestingAgencyId>
|
||||
</header>
|
||||
<bibliographicInfo>
|
||||
<title>Test request - no callback</title>
|
||||
</bibliographicInfo>
|
||||
<serviceInfo>
|
||||
<serviceType>Copy</serviceType>
|
||||
</serviceInfo>
|
||||
</request>
|
||||
XML
|
||||
|
||||
$t->post_ok(
|
||||
"//$userid:$password@/api/v1/public/ill/iso18626",
|
||||
{ 'Content-Type' => 'application/xml' },
|
||||
$request_no_callback_xml
|
||||
)->status_is(201)->content_like(
|
||||
qr{<messageStatus>OK</messageStatus>},
|
||||
'request created successfully'
|
||||
);
|
||||
|
||||
my $request_no_callback = Koha::ILL::ISO18626::Requests->search(
|
||||
{},
|
||||
{ order_by => { -desc => 'iso18626_request_id' }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
warning_like {
|
||||
$t->patch_ok( "//$userid:$password@/api/v1/ill/iso18626_requests/"
|
||||
. $request_no_callback->iso18626_request_id => json => { status => 'Loaned' } )->status_is(200);
|
||||
}
|
||||
qr/ISO18626: Cannot send message/, 'warns when requesting agency has no callback_endpoint';
|
||||
|
||||
# Test: callback_endpoint defined but HTTP request fails
|
||||
$builder->build_object(
|
||||
{
|
||||
class => 'Koha::ILL::ISO18626::RequestingAgencies',
|
||||
value => {
|
||||
account_id => 'bad_endpoint_test',
|
||||
securityCode => 'test_secret_2',
|
||||
callback_endpoint => 'http://localhost/ill/callback'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
my $mock_fail_response = Test::MockObject->new();
|
||||
$mock_fail_response->mock( 'is_success', sub { return 0; } );
|
||||
$mock_fail_response->mock( 'status_line', sub { return '500 Internal Server Error'; } );
|
||||
$mock_fail_response->mock( 'decoded_content', sub { return 'Internal Server Error'; } );
|
||||
|
||||
my $mock_ua_fail = Test::MockModule->new('LWP::UserAgent');
|
||||
$mock_ua_fail->mock( 'post', sub { return $mock_fail_response; } );
|
||||
|
||||
my $request_bad_endpoint_xml = <<'XML';
|
||||
<request xmlns="https://example.com/ill/request">
|
||||
<header>
|
||||
<requestingAgencyAuthentication>
|
||||
<accountId>bad_endpoint_test</accountId>
|
||||
<securityCode>test_secret_2</securityCode>
|
||||
</requestingAgencyAuthentication>
|
||||
<requestingAgencyRequestId>XYZ</requestingAgencyRequestId>
|
||||
<timestamp>2023-03-15 14:30:00</timestamp>
|
||||
<requestingAgencyId>
|
||||
<agencyIdType>ISIL</agencyIdType>
|
||||
<agencyIdValue>req_agency_value</agencyIdValue>
|
||||
</requestingAgencyId>
|
||||
</header>
|
||||
<bibliographicInfo>
|
||||
<title>Test request - bad endpoint</title>
|
||||
</bibliographicInfo>
|
||||
<serviceInfo>
|
||||
<serviceType>Copy</serviceType>
|
||||
</serviceInfo>
|
||||
</request>
|
||||
XML
|
||||
|
||||
$t->post_ok(
|
||||
"//$userid:$password@/api/v1/public/ill/iso18626",
|
||||
{ 'Content-Type' => 'application/xml' },
|
||||
$request_bad_endpoint_xml
|
||||
)->status_is(201)->content_like(
|
||||
qr{<messageStatus>OK</messageStatus>},
|
||||
'request created successfully'
|
||||
);
|
||||
|
||||
my $request_bad_endpoint = Koha::ILL::ISO18626::Requests->search(
|
||||
{},
|
||||
{ order_by => { -desc => 'iso18626_request_id' }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
warning_like {
|
||||
$t->patch_ok( "//$userid:$password@/api/v1/ill/iso18626_requests/"
|
||||
. $request_bad_endpoint->iso18626_request_id => json => { status => 'Loaned' } )->status_is(200);
|
||||
}
|
||||
qr/ISO18626: HTTP Request Failed/, 'warns when HTTP request to callback_endpoint fails';
|
||||
|
||||
# Test: callback_endpoint is set but is not a valid URL
|
||||
$builder->build_object(
|
||||
{
|
||||
class => 'Koha::ILL::ISO18626::RequestingAgencies',
|
||||
value => {
|
||||
account_id => 'invalid_url_test',
|
||||
securityCode => 'test_secret_3',
|
||||
callback_endpoint => 'not-a-valid-url'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
my $request_invalid_url_xml = <<'XML';
|
||||
<request xmlns="https://example.com/ill/request">
|
||||
<header>
|
||||
<requestingAgencyAuthentication>
|
||||
<accountId>invalid_url_test</accountId>
|
||||
<securityCode>test_secret_3</securityCode>
|
||||
</requestingAgencyAuthentication>
|
||||
<requestingAgencyRequestId>XYZ</requestingAgencyRequestId>
|
||||
<timestamp>2023-03-15 14:30:00</timestamp>
|
||||
<requestingAgencyId>
|
||||
<agencyIdType>ISIL</agencyIdType>
|
||||
<agencyIdValue>req_agency_value</agencyIdValue>
|
||||
</requestingAgencyId>
|
||||
</header>
|
||||
<bibliographicInfo>
|
||||
<title>Test request - invalid url</title>
|
||||
</bibliographicInfo>
|
||||
<serviceInfo>
|
||||
<serviceType>Copy</serviceType>
|
||||
</serviceInfo>
|
||||
</request>
|
||||
XML
|
||||
|
||||
$t->post_ok(
|
||||
"//$userid:$password@/api/v1/public/ill/iso18626",
|
||||
{ 'Content-Type' => 'application/xml' },
|
||||
$request_invalid_url_xml
|
||||
)->status_is(201)->content_like(
|
||||
qr{<messageStatus>OK</messageStatus>},
|
||||
'request created successfully'
|
||||
);
|
||||
|
||||
my $request_invalid_url = Koha::ILL::ISO18626::Requests->search(
|
||||
{},
|
||||
{ order_by => { -desc => 'iso18626_request_id' }, rows => 1 }
|
||||
)->single;
|
||||
|
||||
warning_like {
|
||||
$t->patch_ok( "//$userid:$password@/api/v1/ill/iso18626_requests/"
|
||||
. $request_invalid_url->iso18626_request_id => json => { status => 'Loaned' } )->status_is(200);
|
||||
}
|
||||
qr/ISO18626: HTTP Request Failed/, 'warns when callback_endpoint is not a valid URL';
|
||||
|
||||
$schema->storage->txn_rollback;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue