279 lines
11 KiB
Perl
Executable File
279 lines
11 KiB
Perl
Executable File
#!/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 <https://www.gnu.org/licenses>.
|
|
|
|
use Modern::Perl;
|
|
|
|
use Test::NoWarnings;
|
|
use Test::More tests => 7;
|
|
use Test::Mojo;
|
|
|
|
use Mojo::JSON qw( encode_json );
|
|
|
|
use t::lib::TestBuilder;
|
|
use t::lib::Mocks;
|
|
|
|
use C4::Log qw( logaction );
|
|
use Koha::ActionLogs;
|
|
use Koha::Database;
|
|
|
|
my $schema = Koha::Database->new->schema;
|
|
my $builder = t::lib::TestBuilder->new;
|
|
|
|
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
|
|
t::lib::Mocks::mock_preference( 'AuthSuccessLog', 0 );
|
|
t::lib::Mocks::mock_preference( 'AuthFailureLog', 0 );
|
|
t::lib::Mocks::mock_preference( 'BorrowersLog', 0 );
|
|
|
|
my $t = Test::Mojo->new('Koha::REST::V1');
|
|
|
|
# Build credentials and an isolated set of seed log entries used by every
|
|
# subtest below.
|
|
sub build_fixtures {
|
|
|
|
# Need both `tools` (view_system_logs) and `borrowers` so embedded patron
|
|
# fields don't get redacted by Koha::Patron->is_accessible.
|
|
my $librarian = $builder->build_object(
|
|
{
|
|
class => 'Koha::Patrons',
|
|
value => { flags => 2**13 + 2**4 } # tools + borrowers
|
|
}
|
|
);
|
|
my $password = 'thePassword123';
|
|
$librarian->set_password( { password => $password, skip_validation => 1 } );
|
|
|
|
my $patron = $builder->build_object(
|
|
{
|
|
class => 'Koha::Patrons',
|
|
value => { flags => 0 }
|
|
}
|
|
);
|
|
$patron->set_password( { password => $password, skip_validation => 1 } );
|
|
|
|
Koha::ActionLogs->search->delete;
|
|
t::lib::Mocks::mock_userenv( { borrowernumber => $librarian->borrowernumber } );
|
|
|
|
return ( $librarian, $patron, $password );
|
|
}
|
|
|
|
subtest 'authorization' => sub {
|
|
plan tests => 4;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
my $unauth_userid = $patron->userid;
|
|
|
|
$t->get_ok("//$unauth_userid:$password@/api/v1/action_logs")
|
|
->status_is( 403, 'forbidden without view_system_logs' );
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs")->status_is( 200, 'allowed with tools/view_system_logs' );
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
subtest 'simple typed filters' => sub {
|
|
plan tests => 13;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
|
|
logaction( 'MEMBERS', 'MODIFY', $patron->borrowernumber, 'test info', 'INTRANET' );
|
|
logaction( 'CIRCULATION', 'ISSUE', $patron->borrowernumber, 'circ info', 'INTRANET' );
|
|
logaction( 'CATALOGUING', 'ADD', 42, 'biblio added', 'COMMANDLINE' );
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?module=MEMBERS")->status_is(200);
|
|
my $logs = $t->tx->res->json;
|
|
is( scalar @$logs, 1, 'one MEMBERS row' );
|
|
is( $logs->[0]->{module}, 'MEMBERS', 'correct module returned' );
|
|
is( $logs->[0]->{info}, 'test info', 'correct info returned' );
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?action=ADD")->status_is(200);
|
|
is( $t->tx->res->json->[0]->{action}, 'ADD', 'action filter works' );
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?interface=COMMANDLINE")->status_is(200);
|
|
is( $t->tx->res->json->[0]->{interface}, 'COMMANDLINE', 'interface filter works' );
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?bork=borkbork")
|
|
->status_is( 400, 'unsupported query param rejected' );
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
subtest 'q DSL: multi-value, like, range' => sub {
|
|
plan tests => 13;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
|
|
logaction( 'MEMBERS', 'MODIFY', $patron->borrowernumber, 'flag toggled', 'INTRANET' );
|
|
logaction( 'CIRCULATION', 'ISSUE', $patron->borrowernumber, 'item issued', 'INTRANET' );
|
|
logaction( 'CATALOGUING', 'ADD', 42, 'biblio added', 'COMMANDLINE' );
|
|
logaction( 'CATALOGUING', 'MODIFY', 42, 'biblio edited', 'COMMANDLINE' );
|
|
|
|
# Multi-value module via q
|
|
my $q_multi = encode_json( { module => [ 'MEMBERS', 'CIRCULATION' ] } );
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?q=$q_multi")->status_is(200);
|
|
is( scalar @{ $t->tx->res->json }, 2, 'multi-value module filter returns union' );
|
|
|
|
# Substring (LIKE) on info
|
|
my $q_like = encode_json( { info => { -like => '%biblio%' } } );
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?q=$q_like")->status_is(200);
|
|
is( scalar @{ $t->tx->res->json }, 2, 'like operator on info matches both biblio rows' );
|
|
|
|
# Object filter via typed param
|
|
# _match=exact is required: the default 'contains' matching does a LIKE
|
|
# match even on integer columns, so plain ?object=42 would also match
|
|
# object values like 420 or 142.
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?object=42&_match=exact")->status_is(200);
|
|
is( scalar @{ $t->tx->res->json }, 2, 'object filter returns rows for object 42' );
|
|
|
|
# Combined filter
|
|
my $q_combo = encode_json(
|
|
{
|
|
module => 'CATALOGUING',
|
|
action => 'MODIFY'
|
|
}
|
|
);
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?q=$q_combo")->status_is(200);
|
|
my $combo = $t->tx->res->json;
|
|
is( scalar @$combo, 1, 'combined filter narrows to one row' );
|
|
is( $combo->[0]->{action}, 'MODIFY', 'combined filter returned MODIFY' );
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
subtest 'embeds' => sub {
|
|
plan tests => 10;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
|
|
logaction( 'MEMBERS', 'MODIFY', $patron->borrowernumber, 'embed test', 'INTRANET' );
|
|
logaction( 'CATALOGUING', 'ADD', 42, 'biblio', 'COMMANDLINE' );
|
|
|
|
# Without embed, no librarian/patron objects in payload
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?module=MEMBERS")->status_is(200);
|
|
my $bare = $t->tx->res->json->[0];
|
|
ok( !exists $bare->{librarian}, 'no librarian key without embed' );
|
|
ok( !exists $bare->{patron}, 'no patron key without embed' );
|
|
|
|
# With embed=librarian, librarian object populated
|
|
$t->get_ok( "/api/v1/action_logs?module=MEMBERS" =>
|
|
{ 'x-koha-embed' => 'librarian', Authorization => _basic( $userid, $password ) } )->status_is(200);
|
|
my $embed_lib = $t->tx->res->json->[0];
|
|
is(
|
|
$embed_lib->{librarian}->{patron_id},
|
|
$librarian->borrowernumber,
|
|
'embedded librarian.patron_id matches the actor'
|
|
);
|
|
|
|
# With embed=patron, patron object populated for MEMBERS row
|
|
$t->get_ok( "/api/v1/action_logs?module=MEMBERS" =>
|
|
{ 'x-koha-embed' => 'patron', Authorization => _basic( $userid, $password ) } )->status_is(200);
|
|
my $embed_pat = $t->tx->res->json->[0];
|
|
is(
|
|
$embed_pat->{patron}->{patron_id},
|
|
$patron->borrowernumber,
|
|
'embedded patron.patron_id matches the row object'
|
|
);
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
subtest 'item embed (CIRCULATION)' => sub {
|
|
plan tests => 14;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
my $auth = { Authorization => _basic( $userid, $password ) };
|
|
|
|
my $item = $builder->build_sample_item;
|
|
|
|
# ISSUE/RENEWAL log a JSON payload carrying the itemnumber; RETURN logs a
|
|
# bare itemnumber string. Both must resolve to the same embedded item.
|
|
my $json_info = encode_json(
|
|
{
|
|
issue => 1,
|
|
branchcode => $item->homebranch,
|
|
itemnumber => $item->itemnumber,
|
|
confirmations => [],
|
|
forced => [],
|
|
}
|
|
);
|
|
logaction( 'CIRCULATION', 'ISSUE', $patron->borrowernumber, $json_info, 'INTRANET' );
|
|
logaction( 'CIRCULATION', 'RETURN', $patron->borrowernumber, $item->itemnumber, 'INTRANET' );
|
|
logaction( 'MEMBERS', 'MODIFY', $patron->borrowernumber, 'no item here', 'INTRANET' );
|
|
|
|
# JSON payload (ISSUE): item resolved from info.itemnumber
|
|
$t->get_ok( "/api/v1/action_logs?module=CIRCULATION&action=ISSUE" => { 'x-koha-embed' => 'item', %$auth } )
|
|
->status_is(200);
|
|
my $issue = $t->tx->res->json->[0];
|
|
is( $issue->{item}->{item_id}, $item->itemnumber, 'ISSUE: embedded item_id from JSON info' );
|
|
is( $issue->{item}->{external_id}, $item->barcode, 'ISSUE: embedded barcode (external_id)' );
|
|
is( $issue->{item}->{biblio_id}, $item->biblionumber, 'ISSUE: embedded biblio_id' );
|
|
|
|
# Bare itemnumber payload (RETURN)
|
|
$t->get_ok( "/api/v1/action_logs?module=CIRCULATION&action=RETURN" => { 'x-koha-embed' => 'item', %$auth } )
|
|
->status_is(200);
|
|
is( $t->tx->res->json->[0]->{item}->{external_id}, $item->barcode, 'RETURN: item resolved from bare itemnumber' );
|
|
|
|
# Non-CIRCULATION row: item is null even when the embed is requested
|
|
$t->get_ok( "/api/v1/action_logs?module=MEMBERS" => { 'x-koha-embed' => 'item', %$auth } )->status_is(200);
|
|
is( $t->tx->res->json->[0]->{item}, undef, 'non-CIRCULATION row has null item' );
|
|
|
|
# Deleted item: CIRCULATION row whose itemnumber no longer exists -> null
|
|
my $gone = $builder->build_sample_item;
|
|
my $gone_id = $gone->itemnumber;
|
|
$gone->delete;
|
|
logaction(
|
|
'CIRCULATION', 'RENEWAL', $patron->borrowernumber, encode_json( { itemnumber => $gone_id } ),
|
|
'INTRANET'
|
|
);
|
|
$t->get_ok( "/api/v1/action_logs?module=CIRCULATION&action=RENEWAL" => { 'x-koha-embed' => 'item', %$auth } )
|
|
->status_is(200);
|
|
is( $t->tx->res->json->[0]->{item}, undef, 'deleted item resolves to null' );
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
subtest 'pagination and sorting' => sub {
|
|
plan tests => 6;
|
|
$schema->storage->txn_begin;
|
|
|
|
my ( $librarian, $patron, $password ) = build_fixtures();
|
|
my $userid = $librarian->userid;
|
|
|
|
logaction( 'MEMBERS', 'MODIFY', $patron->borrowernumber, "row $_", 'INTRANET' ) for 1 .. 5;
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?_per_page=2&_page=1&module=MEMBERS")->status_is(200);
|
|
is( scalar @{ $t->tx->res->json }, 2, 'pagination limits results' );
|
|
is( $t->tx->res->headers->header('x-total-count'), 5, 'X-Total-Count reflects unfiltered total' );
|
|
|
|
$t->get_ok("//$userid:$password@/api/v1/action_logs?_order_by=-action_id&module=MEMBERS")->status_is(200);
|
|
|
|
$schema->storage->txn_rollback;
|
|
};
|
|
|
|
sub _basic {
|
|
my ( $u, $p ) = @_;
|
|
require MIME::Base64;
|
|
return 'Basic ' . MIME::Base64::encode_base64( "$u:$p", '' );
|
|
}
|