Koha/t/db_dependent/api/v1/patrons.t
Tomas Cohen Arazi 10de4f437f Bug 19784: Unit tests for /api/v1/patrons
This patch adapts the existing endpoint's tests so they expect:
- 'patron_id' for 'borrowernumber'
- 'library_id' for 'branchcode'
- 'category_id' for 'categorycode'

In the process, I tried to make the tests more robust, by creating random
data that gets deleted to make sure our tests cases can't have false
positives.

Independent subtests are wrapped inside transactions to avoid
eventual interference.

To test:
- Apply the patch
- Run:
  $ kshell
 k$ prove t/db_dependent/api/v1/patrons.t
=> FAIL: The api doesn't implement the expected behaviour and attributes
for endpoint responses

Signed-off-by: Josef Moravec <josef.moravec@gmail.com>

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
2018-03-29 11:42:07 -03:00

431 lines
16 KiB
Perl

#!/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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Test::More tests => 5;
use Test::Mojo;
use Test::Warn;
use t::lib::TestBuilder;
use t::lib::Mocks;
use C4::Auth;
use Koha::Database;
use Koha::Patron::Debarments qw/AddDebarment/;
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 'list() tests' => sub {
plan tests => 2;
$schema->storage->txn_begin;
unauthorized_access_tests('GET', undef, undef);
$schema->storage->txn_rollback;
subtest 'librarian access tests' => sub {
plan tests => 13;
$schema->storage->txn_begin;
Koha::Patrons->search->delete;
my ( $patron_id, $session_id ) = create_user_and_session({ authorized => 1 });
my $patron = Koha::Patrons->find($patron_id);
my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200);
$tx = $t->ua->build_tx(GET => '/api/v1/patrons?cardnumber=' . $patron->cardnumber);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200)
->json_is('/0/cardnumber' => $patron->cardnumber);
$tx = $t->ua->build_tx(GET => '/api/v1/patrons?address2='.
$patron->address2);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200)
->json_is('/0/address2' => $patron->address2);
my $patron_2 = $builder->build_object({ class => 'Koha::Patrons' });
AddDebarment({ borrowernumber => $patron_2->id });
# re-read from DB
$patron_2->discard_changes;
my $ub = $patron_2->unblessed;
$tx = $t->ua->build_tx( GET => '/api/v1/patrons?restricted=' . Mojo::JSON->true );
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200)
->json_has('/0/restricted')
->json_is( '/0/restricted' => Mojo::JSON->true )
->json_hasnt('/1');
$schema->storage->txn_rollback;
};
};
subtest 'get() tests' => sub {
plan tests => 3;
$schema->storage->txn_begin;
unauthorized_access_tests('GET', -1, undef);
$schema->storage->txn_rollback;
subtest 'access own object tests' => sub {
plan tests => 4;
$schema->storage->txn_begin;
my ( $patron_id, $session_id ) = create_user_and_session({ authorized => 0 });
# Access patron's own data even though they have no borrowers flag
my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $patron_id);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200);
my $guarantee = $builder->build_object({
class => 'Koha::Patrons',
value => {
guarantorid => $patron_id,
}
});
# Access guarantee's data even though guarantor has no borrowers flag
$tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $guarantee->id );
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$tx->req->env({ REMOTE_ADDR => '127.0.0.1' });
$t->request_ok($tx)
->status_is(200);
$schema->storage->txn_rollback;
};
subtest 'librarian access tests' => sub {
plan tests => 6;
$schema->storage->txn_begin;
my $patron = $builder->build_object({ class => 'Koha::Patrons' });
my ( undef, $session_id ) = create_user_and_session({ authorized => 1 });
my $tx = $t->ua->build_tx(GET => "/api/v1/patrons/" . $patron->id);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(200)
->json_is('/patron_id' => $patron->id)
->json_is('/category_id' => $patron->categorycode )
->json_is('/surname' => $patron->surname)
->json_is('/patron_card_lost' => Mojo::JSON->false );
$schema->storage->txn_rollback;
};
};
subtest 'add() tests' => sub {
plan tests => 2;
$schema->storage->txn_begin;
my $patron = Koha::REST::V1::Patrons::_to_api(
$builder->build_object( { class => 'Koha::Patrons' } )->TO_JSON );
unauthorized_access_tests('POST', undef, $patron);
$schema->storage->txn_rollback;
subtest 'librarian access tests' => sub {
plan tests => 20;
$schema->storage->txn_begin;
my $patron = $builder->build_object({ class => 'Koha::Patrons' });
my $newpatron = Koha::REST::V1::Patrons::_to_api( $patron->TO_JSON );
# delete RO attributes
delete $newpatron->{patron_id};
delete $newpatron->{restricted};
# Create a library just to make sure its ID doesn't exist on the DB
my $library_to_delete = $builder->build_object({ class => 'Koha::Libraries' });
my $deleted_library_id = $library_to_delete->id;
# Delete library
$library_to_delete->delete;
my ( undef, $session_id ) = create_user_and_session({ authorized => 1 });
$newpatron->{library_id} = $deleted_library_id;
my $tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron );
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
warning_like {
$t->request_ok($tx)
->status_is(409)
->json_is('/error' => "Duplicate ID"); }
qr/^DBD::mysql::st execute failed: Duplicate entry/;
$newpatron->{library_id} = $patron->branchcode;
# Create a library just to make sure its ID doesn't exist on the DB
my $category_to_delete = $builder->build_object({ class => 'Koha::Patron::Categories' });
my $deleted_category_id = $category_to_delete->id;
# Delete library
$category_to_delete->delete;
$newpatron->{category_id} = $deleted_category_id; # Test invalid patron category
$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(400)
->json_is('/error' => "Given category_id does not exist");
$newpatron->{category_id} = $patron->categorycode;
$newpatron->{falseproperty} = "Non existent property";
$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(400);
delete $newpatron->{falseproperty};
my $patron_to_delete = $builder->build_object({ class => 'Koha::Patrons' });
$newpatron = Koha::REST::V1::Patrons::_to_api($patron_to_delete->TO_JSON);
# delete RO attributes
delete $newpatron->{patron_id};
delete $newpatron->{restricted};
$patron_to_delete->delete;
$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(201, 'Patron created successfully')
->json_has('/patron_id', 'got a patron_id')
->json_is( '/cardnumber' => $newpatron->{ cardnumber })
->json_is( '/surname' => $newpatron->{ surname })
->json_is( '/firstname' => $newpatron->{ firstname });
$tx = $t->ua->build_tx(POST => "/api/v1/patrons" => json => $newpatron);
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
warning_like {
$t->request_ok($tx)
->status_is(409)
->json_has( '/error', 'Fails when trying to POST duplicate cardnumber' )
->json_has( '/conflict', 'cardnumber' ); }
qr/^DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key 'cardnumber'/;
$schema->storage->txn_rollback;
};
};
subtest 'update() tests' => sub {
plan tests => 2;
$schema->storage->txn_begin;
unauthorized_access_tests('PUT', 123, {email => 'nobody@example.com'});
$schema->storage->txn_rollback;
subtest 'librarian access tests' => sub {
plan tests => 23;
$schema->storage->txn_begin;
t::lib::Mocks::mock_preference('minPasswordLength', 1);
my ( $patron_id_1, $session_id ) = create_user_and_session({ authorized => 1 });
my ( $patron_id_2, undef ) = create_user_and_session({ authorized => 0 });
my $patron_1 = Koha::Patrons->find($patron_id_1);
my $patron_2 = Koha::Patrons->find($patron_id_2);
my $newpatron = Koha::REST::V1::Patrons::_to_api($patron_2->TO_JSON);
# delete RO attributes
delete $newpatron->{patron_id};
delete $newpatron->{restricted};
my $tx = $t->ua->build_tx(PUT => "/api/v1/patrons/-1" => json => $newpatron );
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
$t->request_ok($tx)
->status_is(404)
->json_has('/error', 'Fails when trying to PUT nonexistent patron');
# Create a library just to make sure its ID doesn't exist on the DB
my $category_to_delete = $builder->build_object({ class => 'Koha::Patron::Categories' });
my $deleted_category_id = $category_to_delete->id;
# Delete library
$category_to_delete->delete;
$newpatron->{category_id} = $deleted_category_id;
$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/$patron_id_2" => json => $newpatron );
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
warning_like {
$t->request_ok($tx)
->status_is(400)
->json_is('/error' => "Given category_id does not exist"); }
qr/^DBD::mysql::st execute failed: Cannot add or update a child row: a foreign key constraint fails/;
$newpatron->{category_id} = $patron_2->categorycode;
# Create a library just to make sure its ID doesn't exist on the DB
my $library_to_delete = $builder->build_object({ class => 'Koha::Libraries' });
my $deleted_library_id = $library_to_delete->id;
# Delete library
$library_to_delete->delete;
$newpatron->{library_id} = $deleted_library_id;
$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $patron_2->id => json => $newpatron );
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
warning_like {
$t->request_ok($tx)
->status_is(400)
->json_is('/error' => "Given library_id does not exist"); }
qr/^DBD::mysql::st execute failed: Cannot add or update a child row: a foreign key constraint fails/;
$newpatron->{library_id} = $patron_2->branchcode;
$newpatron->{falseproperty} = "Non existent property";
$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $patron_2->id => json => $newpatron );
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
$t->request_ok($tx)
->status_is(400)
->json_is('/errors/0/message' =>
'Properties not allowed: falseproperty.');
delete $newpatron->{falseproperty};
# Set both cardnumber and userid to already existing values
$newpatron->{cardnumber} = $patron_1->cardnumber;
$newpatron->{userid} = $patron_1->userid;
$tx = $t->ua->build_tx( PUT => "/api/v1/patrons/" . $patron_2->id => json => $newpatron );
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
warning_like {
$t->request_ok($tx)
->status_is(409)
->json_has( '/error' => "Fails when trying to update to an existing cardnumber or userid")
->json_is( '/conflict', 'cardnumber' ); }
qr/^DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key 'cardnumber'/;
$newpatron->{ cardnumber } = $patron_id_1.$patron_id_2;
$newpatron->{ userid } = "user".$patron_id_1.$patron_id_2;
$newpatron->{ surname } = "user".$patron_id_1.$patron_id_2;
$tx = $t->ua->build_tx(PUT => "/api/v1/patrons/" . $patron_2->id => json => $newpatron);
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
$t->request_ok($tx)
->status_is(200, 'Patron updated successfully')
->json_has($newpatron);
is(Koha::Patrons->find( $patron_2->id )->cardnumber,
$newpatron->{ cardnumber }, 'Patron is really updated!');
$schema->storage->txn_rollback;
};
};
subtest 'delete() tests' => sub {
plan tests => 2;
$schema->storage->txn_begin;
unauthorized_access_tests('DELETE', 123, undef);
$schema->storage->txn_rollback;
subtest 'librarian access test' => sub {
plan tests => 4;
$schema->storage->txn_begin;
my ( undef, $session_id ) = create_user_and_session({ authorized => 1 });
my ( $patron_id, undef ) = create_user_and_session({ authorized => 0 });
my $tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/-1");
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(404, 'Patron not found');
$tx = $t->ua->build_tx(DELETE => "/api/v1/patrons/$patron_id");
$tx->req->cookies({ name => 'CGISESSID', value => $session_id });
$t->request_ok($tx)
->status_is(200, 'Patron deleted successfully');
$schema->storage->txn_rollback;
};
};
# Centralized tests for 401s and 403s assuming the endpoint requires
# borrowers flag for access
sub unauthorized_access_tests {
my ($verb, $patron_id, $json) = @_;
my $endpoint = '/api/v1/patrons';
$endpoint .= ($patron_id) ? "/$patron_id" : '';
subtest 'unauthorized access tests' => sub {
plan tests => 5;
my $tx = $t->ua->build_tx($verb => $endpoint => json => $json);
$t->request_ok($tx)
->status_is(401);
my ($borrowernumber, $session_id) = create_user_and_session({
authorized => 0 });
$tx = $t->ua->build_tx($verb => $endpoint => json => $json);
$tx->req->cookies({name => 'CGISESSID', value => $session_id});
$t->request_ok($tx)
->status_is(403)
->json_has('/required_permissions');
};
}
sub create_user_and_session {
my $args = shift;
my $flags = ( $args->{authorized} ) ? 16 : 0;
my $user = $builder->build(
{
source => 'Borrower',
value => {
flags => $flags,
gonenoaddress => 0,
lost => 0,
email => 'nobody@example.com',
emailpro => 'nobody@example.com',
B_email => 'nobody@example.com'
}
}
);
# 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;
return ( $user->{borrowernumber}, $session->id );
}