From bb7dc73c873aac3fe0d469bccec1dff7e576a491 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Mon, 23 Mar 2015 20:14:23 +0100 Subject: [PATCH] Bug 13920: 7. API Authentication, part 1: API keys management in interface Depends on Buugg 14539. This introduces the concept of API keys for use in the new REST API. A key is a string of 32 alphanumerical characters (32 is purely arbitrary, it can be changed easily). A user can have multiple keys (unlimited at the moment) Keys can be generated automatically, and then we have the possibility to delete or revoke each one individually. ApiKeys can be easily accessed using the Koha::ApiKeys-package. Includes unit tests and selenium integration tests for Intra and OPAC. Test plan: 1/ Go to staff interface 2/ Go to a borrower page 3/ In toolbar, click on More -> Manage API keys 4/ Click on "Generate new key" multiple times, check that they are correctly displayed under the button, and they are active by default 5/ Revoke some keys, check that they are not active anymore 6/ Delete some keys, check that they disappear from table 7/ Go to opac interface, log in 8/ In your user account pages, you now have a new tab to the left "your API keys". Click on it. 9/ Repeat steps 4-6 --- Koha/ApiKey.pm | 46 +++++ Koha/ApiKeys.pm | 121 +++++++++++++ Koha/Borrower.pm | 32 ++++ Koha/Schema/Result/ApiKey.pm | 122 +++++++++++++ Koha/Schema/Result/Borrower.pm | 19 +- installer/data/mysql/kohastructure.sql | 19 ++ installer/data/mysql/updatedatabase.pl | 30 ++++ .../prog/en/includes/members-toolbar.inc | 5 + .../prog/en/modules/members/apikeys.tt | 78 ++++++++ .../opac-tmpl/bootstrap/en/includes/usermenu.inc | 8 + .../opac-tmpl/bootstrap/en/modules/opac-apikeys.tt | 82 +++++++++ members/apikeys.pl | 82 +++++++++ opac/opac-apikeys.pl | 81 +++++++++ t/db_dependent/Koha/ApiKeys.t | 124 +++++++++++++ t/lib/Page/Members/ApiKeys.pm | 200 +++++++++++++++++++++ t/lib/Page/Members/MemberFlags.pm | 2 +- t/lib/Page/Members/Moremember.pm | 6 +- t/lib/Page/Members/Toolbar.pm | 135 ++++++++++++++ t/lib/Page/Opac/LeftNavigation.pm | 109 +++++++++++ t/lib/Page/Opac/OpacApiKeys.pm | 170 ++++++++++++++++++ t/lib/Page/Opac/OpacSearchHistory.pm | 2 +- t/lib/Page/Opac/OpacUser.pm | 4 +- 22 files changed, 1454 insertions(+), 23 deletions(-) create mode 100644 Koha/ApiKey.pm create mode 100644 Koha/ApiKeys.pm create mode 100644 Koha/Schema/Result/ApiKey.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt create mode 100755 members/apikeys.pl create mode 100755 opac/opac-apikeys.pl create mode 100644 t/db_dependent/Koha/ApiKeys.t create mode 100644 t/lib/Page/Members/ApiKeys.pm create mode 100644 t/lib/Page/Members/Toolbar.pm create mode 100644 t/lib/Page/Opac/LeftNavigation.pm create mode 100644 t/lib/Page/Opac/OpacApiKeys.pm diff --git a/Koha/ApiKey.pm b/Koha/ApiKey.pm new file mode 100644 index 0000000..be4b76e --- /dev/null +++ b/Koha/ApiKey.pm @@ -0,0 +1,46 @@ +package Koha::ApiKey; + +# Copyright BibLibre 2015 +# +# 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 Carp; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ApiKey - Koha API Key Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiKey'; +} + +1; diff --git a/Koha/ApiKeys.pm b/Koha/ApiKeys.pm new file mode 100644 index 0000000..6a24111 --- /dev/null +++ b/Koha/ApiKeys.pm @@ -0,0 +1,121 @@ +package Koha::ApiKeys; + +# Copyright BibLibre 2015 +# +# 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 Scalar::Util qw(blessed); + +use Koha::Borrowers; +use Koha::ApiKey; + +use base qw(Koha::Objects); + +use Koha::Exception::BadParameter; +use Koha::Exception::UnknownObject; + +=head1 NAME + +Koha::ApiKeys - Koha API Keys Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiKey'; +} + +sub object_class { + return 'Koha::ApiKey'; +} + +sub _get_castable_unique_columns { + return ['api_key_id', 'api_key']; +} + +=head grant + + my $apiKey = Koha::ApiKey->grant({borrower => $borrower, + apiKey => $apiKey + }); + +Granting an ApiKey should be easy. This creates a new ApiKey for the given Borrower, +or sets the owner of an existing key. +$PARAM1 HASHRef of params, { + borrower => MANDATORY, a Koha::Borrower or something castable to one. + apiKey => OPTIONAL, an existing Koha::ApiKEy to give to somebody else. + not sure why anybody would want to do that, but + provided as a convenience for testing. +} +@THROWS Koha::Exception::BadParameter +=cut + +sub grant { + my ($self, $borrower, $apiKey) = @_; + $borrower = Koha::Borrowers->cast($borrower); + if ($apiKey) { + $apiKey = Koha::ApiKeys->cast($apiKey); + $apiKey->borrowernumber($borrower->borrowernumber); + } + else { + $apiKey = new Koha::ApiKey; + $apiKey->borrowernumber($borrower->borrowernumber); + $apiKey->api_key(String::Random->new->randregex('[a-zA-Z0-9]{32}')); + } + $apiKey->store; + return $apiKey; +} + +sub delete { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->delete; + } +} + +sub revoke { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->active(0); + $apiKey->store; + } + return $apiKey; +} + +sub activate { + my ($self, $apiKey) = @_; + $apiKey = Koha::ApiKeys->cast($apiKey); + + if ($apiKey) { + $apiKey->active(1); + $apiKey->store; + } + return $apiKey; +} + +1; diff --git a/Koha/Borrower.pm b/Koha/Borrower.pm index 4ceae11..3f71df6 100644 --- a/Koha/Borrower.pm +++ b/Koha/Borrower.pm @@ -69,6 +69,38 @@ sub isSuperuser { return (exists($self->{superuser}) && $self->{superuser}) ? 1 : undef; } +=head getApiKeys + + my @apiKeys = $borrower->getApiKeys( $activeOnly ); + +=cut + +sub getApiKeys { + my ($self, $activeOnly) = @_; + + my @dbix_objects = $self->_result()->api_keys({active => 1}); + for (my $i=0 ; $i_new_from_dbic($dbix_objects[$i]); + } + + return \@dbix_objects; +} + +=head getApiKey + + my $apiKey = $borrower->getApiKeys( $activeOnly ); + +=cut + +sub getApiKey { + my ($self, $activeOnly) = @_; + + my $dbix_object = $self->_result()->api_keys({active => 1})->next(); + my $object = Koha::ApiKey->_new_from_dbic($dbix_object); + + return $object; +} + =head1 AUTHOR Kyle M Hall diff --git a/Koha/Schema/Result/ApiKey.pm b/Koha/Schema/Result/ApiKey.pm new file mode 100644 index 0000000..e5b360c --- /dev/null +++ b/Koha/Schema/Result/ApiKey.pm @@ -0,0 +1,122 @@ +use utf8; +package Koha::Schema::Result::ApiKey; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::ApiKey + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("api_keys"); + +=head1 ACCESSORS + +=head2 api_key_id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 borrowernumber + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 api_key + + data_type: 'varchar' + is_nullable: 0 + size: 255 + +=head2 last_request_time + + data_type: 'integer' + default_value: 0 + is_nullable: 1 + +=head2 active + + data_type: 'integer' + default_value: 1 + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "api_key_id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "borrowernumber", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "api_key", + { data_type => "varchar", is_nullable => 0, size => 255 }, + "last_request_time", + { data_type => "integer", default_value => 0, is_nullable => 1 }, + "active", + { data_type => "integer", default_value => 1, is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("api_key_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("apk_bornumkey_idx", ["borrowernumber", "api_key"]); + +=head1 RELATIONS + +=head2 borrowernumber + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "borrowernumber", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-31 11:03:00 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8nljlCCakQs1X0kTmW7PYw + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/Koha/Schema/Result/Borrower.pm b/Koha/Schema/Result/Borrower.pm index 4d7122d..4ce93b8 100644 --- a/Koha/Schema/Result/Borrower.pm +++ b/Koha/Schema/Result/Borrower.pm @@ -656,21 +656,6 @@ __PACKAGE__->has_many( { cascade_copy => 0, cascade_delete => 0 }, ); -=head2 api_timestamp - -Type: might_have - -Related object: L - -=cut - -__PACKAGE__->might_have( - "api_timestamp", - "Koha::Schema::Result::ApiTimestamp", - { "foreign.borrowernumber" => "self.borrowernumber" }, - { cascade_copy => 0, cascade_delete => 0 }, -); - =head2 aqbasketusers Type: has_many @@ -1177,8 +1162,8 @@ Composing rels: L -> ordernumber __PACKAGE__->many_to_many("ordernumbers", "aqorder_users", "ordernumber"); -# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-22 14:11:54 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:r9btWrDMY5+WtoACFGuchA +# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-07-31 11:03:00 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:geKoy0EPGyI2MHZQAhL0MA # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index a1c7510..48fac7b 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -16,6 +16,25 @@ /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- +-- Table structure for table api_keys +-- + +DROP TABLE IF EXISTS api_keys; +CREATE TABLE api_keys ( + api_key_id INT(11) NOT NULL auto_increment, + borrowernumber INT(11) NOT NULL, -- foreign key to the borrowers table + api_key VARCHAR(255) NOT NULL, -- API key used for API authentication + last_request_time INT(11) default 0, -- UNIX timestamp of when was the last transaction for this API-key? Used for request replay control. + active INT(1) DEFAULT 1, -- 0 means this API key is revoked + PRIMARY KEY (api_key_id), + UNIQUE KEY apk_bornumkey_idx (borrowernumber, api_key), + CONSTRAINT api_keys_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `auth_header` -- diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index c4b191e..7fb915b 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -10730,6 +10730,36 @@ if ( CheckVersion($DBversion) ) { SetVersion ($DBversion); } +$DBversion = "XXX"; +if(CheckVersion($DBversion)) { + $dbh->do(q{ + DROP TABLE IF EXISTS api_keys; + }); + $dbh->do(q{ + CREATE TABLE api_keys ( + api_key_id INT(11) NOT NULL auto_increment, + borrowernumber INT(11) NOT NULL, -- foreign key to the borrowers table + api_key VARCHAR(255) NOT NULL, -- API key used for API authentication + last_request_time INT(11) default 0, -- UNIX timestamp of when was the last transaction for this API-key? Used for request replay control. + active INT(1) DEFAULT 1, -- 0 means this API key is revoked + PRIMARY KEY (api_key_id), + UNIQUE KEY apk_bornumkey_idx (borrowernumber, api_key), + CONSTRAINT api_keys_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + }); + + use Koha::Auth::PermissionManager; + my $pm = Koha::Auth::PermissionManager->new(); + $pm->addPermission({code => 'manage_api_keys', description => "Manage Borrowers' REST API keys"}); + + print "Upgrade to $DBversion done (Bug 13920: Add API keys table)\n"; + SetVersion($DBversion); +} + + # DEVELOPER PROCESS, search for anything to execute in the db_update directory # SEE bug 13068 # if there is anything in the atomicupdate, read and execute it. diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc index 70edbc0..a3155cb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -170,6 +170,11 @@ function searchToHold(){ [% ELSE %]
  • Set permissions
  • [% END %] + [% IF ( CAN_user_borrowers_manage_api_keys ) %] +
  • Manage API keys
  • + [% ELSE %] +
  • Manage API keys
  • + [% END %] [% IF ( CAN_user_borrowers ) %] [% IF ( NorwegianPatronDBEnable == 1 ) %]
  • Delete local
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt new file mode 100644 index 0000000..7fdd416 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/apikeys.tt @@ -0,0 +1,78 @@ +[% USE Koha %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Patrons › API Keys +[% INCLUDE 'doc-head-close.inc' %] + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'patron-search.inc' %] + + + +
    +
    +
    +
    + [% INCLUDE 'members-toolbar.inc' %] + +

    API keys for [% INCLUDE 'patron-title.inc' %]

    +
    +
    + + + +
    +
    + [% IF api_keys.size > 0 %] + + + + + + + + + + + [% FOREACH key IN api_keys %] + + + + + + + [% END %] + +
    KeyActiveLast transactionActions
    [% key.api_key %][% IF key.active %]Yes[% ELSE %]No[% END %][% key.last_request_time || '' %] +
    + + + + +
    +
    + + + [% IF key.active %] + + + [% ELSE %] + + + [% END %] +
    +
    + [% END %] +
    +
    +
    + [% INCLUDE 'circ-menu.inc' %] +
    +
    +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc index c5a2298..5e6fefa 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc @@ -1,3 +1,4 @@ +[% USE Koha %] [% IF ( ( Koha.Preference( 'opacuserlogin' ) == 1 ) && loggedinusername ) %] [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt new file mode 100644 index 0000000..297aa2a --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-apikeys.tt @@ -0,0 +1,82 @@ +[% INCLUDE 'doc-head-open.inc' %] +[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › Your library home › Your API keys +[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] + +[% INCLUDE 'bodytag.inc' bodyid='opac-user' bodyclass='scrollto' %] +[% INCLUDE 'masthead.inc' %] + +
    + + +
    +
    +
    + +
    +
    +
    +

    Your API keys

    +
    +
    + + +
    +
    + [% IF api_keys.size > 0 %] + + + + + + + + + + + [% FOREACH key IN api_keys %] + + + + + + + [% END %] + +
    KeyActiveLast transactionActions
    [% key.api_key %][% IF key.active %]Yes[% ELSE %]No[% END %][% key.last_request_time || '' %] +
    + + + +
    +
    + + [% IF key.active %] + + + [% ELSE %] + + + [% END %] +
    +
    + [% END %] +
    +
    +
    +
    +
    + +[% BLOCK jsinclude %][% END %] +[% INCLUDE 'opac-bottom.inc' %] diff --git a/members/apikeys.pl b/members/apikeys.pl new file mode 100755 index 0000000..ac358be --- /dev/null +++ b/members/apikeys.pl @@ -0,0 +1,82 @@ +#!/usr/bin/perl + +# Copyright 2015 BibLibre +# +# 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 2 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 CGI; +use String::Random; + +use C4::Auth; +use C4::Members; +use C4::Output; +use Koha::ApiKeys; +use Koha::ApiKey; + +my $cgi = new CGI; + +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => 'members/apikeys.tt', + query => $cgi, + type => 'intranet', + authnotrequired => 0, + flagsrequired => {borrowers => 'manage_api_keys'}, +}); + +my $borrowernumber = $cgi->param('borrowernumber'); +my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); +my $op = $cgi->param('op'); + +if ($op) { + if ($op eq 'generate') { + Koha::ApiKeys->grant($borrower); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'delete') { + my $key = $cgi->param('key'); + Koha::ApiKeys->delete($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'revoke') { + my $key = $cgi->param('key'); + Koha::ApiKeys->revoke($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } + + if ($op eq 'activate') { + my $key = $cgi->param('key'); + Koha::ApiKeys->activate($key); + print $cgi->redirect('/cgi-bin/koha/members/apikeys.pl?borrowernumber=' . $borrowernumber); + exit; + } +} + +my @api_keys = Koha::ApiKeys->search({borrowernumber => $borrowernumber}); + +$template->param( + api_keys => \@api_keys, + borrower => $borrower, + borrowernumber => $borrowernumber, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; diff --git a/opac/opac-apikeys.pl b/opac/opac-apikeys.pl new file mode 100755 index 0000000..72cc70c --- /dev/null +++ b/opac/opac-apikeys.pl @@ -0,0 +1,81 @@ +#!/usr/bin/env perl + +# Copyright 2015 BibLibre +# +# 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 2 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 CGI; +use String::Random; + +use C4::Auth; +use C4::Members; +use C4::Output; +use Koha::ApiKeys; + +my $cgi = new CGI; + +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => 'opac-apikeys.tt', + query => $cgi, + type => 'opac', + authnotrequired => 0, +}); + +my $borrowernumber = $loggedinuser; +my $borrower = C4::Members::GetMember(borrowernumber => $borrowernumber); +my $op = $cgi->param('op'); + +if ($op) { + if ($op eq 'generate') { + Koha::ApiKeys->grant($borrower); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'delete') { + my $key = $cgi->param('key'); + Koha::ApiKeys->delete($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'revoke') { + my $key = $cgi->param('key'); + Koha::ApiKeys->revoke($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } + + if ($op eq 'activate') { + my $key = $cgi->param('key'); + Koha::ApiKeys->activate($key); + print $cgi->redirect('/cgi-bin/koha/opac-apikeys.pl'); + exit; + } +} + +my @api_keys = Koha::ApiKeys->search({borrowernumber => $borrowernumber}); + +$template->param( + apikeysview => 1, + api_keys => \@api_keys, + borrower => $borrower, + borrowernumber => $borrowernumber, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; diff --git a/t/db_dependent/Koha/ApiKeys.t b/t/db_dependent/Koha/ApiKeys.t new file mode 100644 index 0000000..19fcd70 --- /dev/null +++ b/t/db_dependent/Koha/ApiKeys.t @@ -0,0 +1,124 @@ +#!/usr/bin/perl + +# Copyright 2015 Vaara-kirjastot +# +# 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; +use Try::Tiny; +use Scalar::Util qw(blessed); + +use Koha::Auth::PermissionManager; +use Koha::ApiKeys; + +use t::lib::TestObjects::ObjectFactory; +use t::lib::TestObjects::BorrowerFactory; +use t::lib::Page::Members::Moremember; +use t::lib::Page::Opac::OpacMain; + +##Setting up the test context +my $testContext = {}; + +my $password = '1234'; +my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new(); +my $borrowers = $borrowerFactory->createTestGroup([ + {firstname => 'Olli-Antti', + surname => 'Kivi', + cardnumber => '1A01', + branchcode => 'CPL', + password => $password, + }, + {firstname => 'Alli-Ontti', + surname => 'Ivik', + cardnumber => '1A02', + branchcode => 'CPL', + password => $password, + }, + ], undef, $testContext); +my $borrowerKivi = $borrowers->{'1A01'}; +my $borrowerIvik = $borrowers->{'1A02'}; +my $permissionManager = Koha::Auth::PermissionManager->new(); +$permissionManager->grantPermission($borrowerKivi, 'borrowers', 'manage_api_keys'); + + +##Test context set, starting testing: +eval { #run in a eval-block so we don't die without tearing down the test context +subtest "ApiKeys API Unit tests" => sub { + my $borrowerKivi = $borrowers->{'1A01'}; + my $borrowerIvik = $borrowers->{'1A02'}; + + + my $apiKey = Koha::ApiKeys->grant($borrowerKivi); + is($apiKey->borrowernumber, $borrowerKivi->borrowernumber, "ApiKey granted"); + + Koha::ApiKeys->revoke($apiKey); + is($apiKey->active, 0, "ApiKey revoked"); + + Koha::ApiKeys->activate($apiKey); + is($apiKey->active, 1, "ApiKey activated"); + + Koha::ApiKeys->grant($borrowerIvik, $apiKey); + is($apiKey->borrowernumber, $borrowerIvik->borrowernumber, "ApiKey granted to another Borrower"); + + Koha::ApiKeys->delete($apiKey); + $apiKey = Koha::ApiKeys->find({api_key_id => $apiKey->api_key_id}); + ok(not($apiKey), "ApiKey deleted"); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +eval { +subtest "ApiKeys Intra Integration tests" => sub { + my $agent = t::lib::Page::Members::Moremember->new({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->doPasswordLogin($borrowerKivi->userid, $password)->navigateManageApiKeys()->generateNewApiKey(); + my @apiKeys = Koha::ApiKeys->search({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->revokeApiKey($apiKeys[0]->api_key)->deleteApiKey($apiKeys[0]->api_key) + ->quit(); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +eval { +subtest "ApiKeys OPAC Integration tests" => sub { + my $agent = t::lib::Page::Opac::OpacMain->new(); + $agent->doPasswordLogin($borrowerKivi->userid, $password)->navigateYourAPIKeys()->generateNewApiKey(); + my @apiKeys = Koha::ApiKeys->search({borrowernumber => $borrowerKivi->borrowernumber}); + $agent->revokeApiKey($apiKeys[0]->api_key)->deleteApiKey($apiKeys[0]->api_key) + ->quit(); +} +}; +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +##All tests done, tear down test context +tearDown(); +done_testing; + +sub tearDown { + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); +} \ No newline at end of file diff --git a/t/lib/Page/Members/ApiKeys.pm b/t/lib/Page/Members/ApiKeys.pm new file mode 100644 index 0000000..6d57ba2 --- /dev/null +++ b/t/lib/Page/Members/ApiKeys.pm @@ -0,0 +1,200 @@ +package t::lib::Page::Members::ApiKeys; + +# Copyright 2015 KohaSuomi! +# +# 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 Scalar::Util qw(blessed); +use Test::More; + +use base qw(t::lib::Page::Intra t::lib::Page::Members::Toolbar); + +use Koha::Exception::BadParameter; +use Koha::Exception::UnknownObject; + +=head NAME t::lib::Page::Members::ApiKeys + +=head SYNOPSIS + +apikeys.pl PageObject providing page functionality as a service! + +=cut + +=head new + + my $apikeys = t::lib::Page::Members::ApiKeys->new({borrowernumber => "1"}); + +Instantiates a WebDriver and loads the members/apikeys.pl. +@PARAM1 HASHRef of optional and MANDATORY parameters +MANDATORY extra parameters: + borrowernumber => loads the page to display Borrower matching the given borrowernumber + +@RETURNS t::lib::Page::Members::ApiKeys, ready for user actions! +=cut + +sub new { + my ($class, $params) = @_; + unless (ref($params) eq 'HASH' || (blessed($params) && $params->isa('t::lib::Page') )) { + $params = {}; + } + $params->{resource} = '/cgi-bin/koha/members/apikeys.pl'; + $params->{type} = 'staff'; + + $params->{getParams} = []; + #Handle MANDATORY parameters + if ($params->{borrowernumber}) { + push @{$params->{getParams}}, "borrowernumber=".$params->{borrowernumber}; + } + else { + Koha::Exception::BadParameter->throw(error => __PACKAGE__."->new():> Parameter 'borrowernumber' is missing."); + } + + my $self = $class->SUPER::new($params); + + return $self; +} + +################################################################################ +=head UI Mapping helper subroutines +See. Selenium documentation best practices for UI element mapping to common language descriptions. +=cut +################################################################################ + +=head _getActionsAndTableElements +@RETURNS List of + HASHRef of Selenium::Driver::Webelement-objects matching the generic + actions on this page, eg. 'generateNewKey'. + HASHRef of Selenium::Driver::Webelement-objects keyed with the apiKey hash/text. + These are all the apiKey table rows present, and have the + elements prefetched for easy access. + +=cut + +sub _getActionsAndTableElements { + my ($self) = @_; + my $d = $self->getDriver(); + + my $generateNewKeySubmit = $d->find_element("#generatenewkey", 'css'); + + my $a = {}; #Collect action elements here + $a->{generateNewKey} = $generateNewKeySubmit; #Bind the global action here for easy reference. + + my $apiKeyRows; + eval { #We might not have ApiKeys yet. + $apiKeyRows = $d->find_elements("#apikeystable tr", 'css'); + shift @$apiKeyRows; #Remove the table header row + }; + my %apiKeys; + for(my $i=0 ; $i[$i]; + $row->{'nth-of-type'} = $i+1; #starts from 1 + $row->{key} = $d->find_child_element($row, "td.apikeykey", 'css')->get_text(); + $row->{active} = $d->find_child_element($row, "td.apikeyactive", 'css')->get_text(); + $row->{lastTransaction} = $d->find_child_element($row, "td.apikeylastransaction", 'css')->get_text(); + $row->{delete} = $d->find_child_element($row, "input.apikeydelete", 'css'); + eval { + $row->{revoke} = $d->find_child_element($row, "input.apikeyrevoke", 'css'); + }; + eval { + $row->{activate} = $d->find_child_element($row, "input.apikeyactivate", 'css'); + }; + $apiKeys{$row->{key}} = $row; + } + + return ($a, \%apiKeys); +} + + + +################################################################################ +=head PageObject Services + +=cut +################################################################################ + +sub generateNewApiKey { + my ($self) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPre = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + $actionElements->{generateNewKey}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPost = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + is($apiKeyRowsCountPre+1, $apiKeyRowsCountPost, "ApiKey generated"); + return $self; +} + +sub revokeApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{revoke}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found after revoking it.") unless $apiKeyRow; + is($apiKeyRow->{active}, 'No', "ApiKey revoked"); + return $self; +} + +sub activateApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{activate}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found after activating it.") unless $apiKeyRow; + is($apiKeyRow->{active}, 'Yes', "ApiKey activated"); + return $self; +} + +sub deleteApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPre = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{delete}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPost = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + is($apiKeyRowsCountPre-1, $apiKeyRowsCountPost, "ApiKey deleted"); + return $self; +} + +1; #Make the compiler happy! diff --git a/t/lib/Page/Members/MemberFlags.pm b/t/lib/Page/Members/MemberFlags.pm index 60448bd..3fc93a5 100644 --- a/t/lib/Page/Members/MemberFlags.pm +++ b/t/lib/Page/Members/MemberFlags.pm @@ -22,7 +22,7 @@ use Test::More; use t::lib::Page::Members::Moremember; -use base qw(t::lib::Page::Intra); +use base qw(t::lib::Page::Intra t::lib::Page::Members::Toolbar); =head NAME t::lib::Page::Members::MemberFlags diff --git a/t/lib/Page/Members/Moremember.pm b/t/lib/Page/Members/Moremember.pm index 82afa62..456fd19 100644 --- a/t/lib/Page/Members/Moremember.pm +++ b/t/lib/Page/Members/Moremember.pm @@ -20,7 +20,9 @@ package t::lib::Page::Members::Moremember; use Modern::Perl; use Scalar::Util qw(blessed); -use base qw(t::lib::Page::Intra); +use base qw(t::lib::Page::Intra t::lib::Page::Members::Toolbar); + +use t::lib::Page::Members::ApiKeys; use Koha::Exception::BadParameter; @@ -86,4 +88,4 @@ See. Selenium documentation best practices for UI element mapping to common lang -1; #Make the compiler happy! \ No newline at end of file +1; #Make the compiler happy! diff --git a/t/lib/Page/Members/Toolbar.pm b/t/lib/Page/Members/Toolbar.pm new file mode 100644 index 0000000..2bd8b84 --- /dev/null +++ b/t/lib/Page/Members/Toolbar.pm @@ -0,0 +1,135 @@ +package t::lib::Page::Members::Toolbar; + +# Copyright 2015 KohaSuomi! +# +# 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; + +use Koha::Exception::BadParameter; + +=head NAME t::lib::Page::Members::Toolbar + +=head SYNOPSIS + +PageObject Accessory encapsulating specific page-related functionality. +Is a static class and should not be attached to the instantiated PageObject. +this way we preserve the interoperability of the UserAgent when it moves from page +to page. + +In this case this encapsulates the members-module toolbar's services and provides +a reusable class from all member-module PageObjects. + +=cut + +################################################################################ +=head UI Mapping helper subroutines +See. Selenium documentation best practices for UI element mapping to common language descriptions. +=cut +################################################################################ + +=head _getToolbarActionElements +Shares the same toolbar with moremember.pl +@RETURNS HASHRef of Selenium::Driver::Webelements matching all the clickable elements + in the actions toolbar over the Borrower information. +=cut + +sub _getToolbarActionElements { + my ($self) = @_; + my $d = $self->getDriver(); + + my $editA = $d->find_element("#editpatron", 'css'); + my $changePasswordA = $d->find_element("#changepassword", 'css'); + my $duplicateA = $d->find_element("#duplicate", 'css'); + my $printButton = $d->find_element("#duplicate + div > button", 'css'); + my $searchToHoldA = $d->find_element("#searchtohold", 'css'); + my $moreButton = $d->find_element("#searchtohold + div > button", 'css'); + + my $e = {}; + $e->{edit} = $editA if $editA; + $e->{changePassword} = $changePasswordA if $changePasswordA; + $e->{duplicate} = $duplicateA if $duplicateA; + $e->{print} = $printButton if $printButton; + $e->{searchToHold} = $searchToHoldA if $searchToHoldA; + $e->{more} = $moreButton if $moreButton; + return $e; +} + +=head _getMoreDropdownElements +Clicks the dropdown open if it isnt yet. +@RETURNS HASHRef of all the dropdown elements under the More button in the toolbar + over Borrower information. +=cut + +sub _getMoreDropdownElements { + my ($self) = @_; + my $d = $self->getDriver(); + + my $toolbarElements = $self->_getToolbarActionElements(); + my $moreButton = $toolbarElements->{more}; + my $deleteA; + eval { + $deleteA = $d->find_child_element($moreButton, "#deletepatron", 'css'); + }; + unless ($deleteA && $deleteA->is_visible()) { + $moreButton->click(); + $self->debugTakeSessionSnapshot(); + } + + my $renewPatronA = $d->find_element("#renewpatron", 'css'); + my $setPermissionsA = $d->find_element("#patronflags", 'css'); + my $manageApiKeysA = $d->find_element("#apikeys", 'css'); + $deleteA = $d->find_element("#deletepatron", 'css'); + $self->debugTakeSessionSnapshot(); + my $updateChildToAdultPatronA = $d->find_element("#updatechild", 'css'); + my $exportCheckinBarcodesA = $d->find_element("#exportcheckins", 'css'); + + my $e = {}; + $e->{renewPatron} = $renewPatronA if $renewPatronA; + $e->{setPermissions} = $setPermissionsA if $setPermissionsA; + $e->{manageApiKeys} = $manageApiKeysA if $manageApiKeysA; + $e->{delete} = $deleteA if $deleteA; + $e->{updateChildToAdultPatron} = $updateChildToAdultPatronA if $updateChildToAdultPatronA; + $e->{exportCheckinBarcodes} = $exportCheckinBarcodesA if $exportCheckinBarcodesA; + return $e; +} + + +################################################################################ +=head PageObject Services + +=cut +################################################################################ + +sub navigateManageApiKeys { + my ($self) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my $elements = $self->_getMoreDropdownElements(); + $elements->{manageApiKeys}->click(); + ok($d->get_title() =~ m/API Keys/, "Intra Navigate to Manage API Keys"); + + $self->debugTakeSessionSnapshot(); + + return t::lib::Page::Members::ApiKeys->rebrandFromPageObject($self); +} + + + + +1; #Make the compiler happy! \ No newline at end of file diff --git a/t/lib/Page/Opac/LeftNavigation.pm b/t/lib/Page/Opac/LeftNavigation.pm new file mode 100644 index 0000000..f39c685 --- /dev/null +++ b/t/lib/Page/Opac/LeftNavigation.pm @@ -0,0 +1,109 @@ +package t::lib::Page::Opac::LeftNavigation; + +# Copyright 2015 Open Source Freedom Fighters +# +# 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 Scalar::Util qw(blessed); +use Test::More; + +use t::lib::Page::Opac::OpacApiKeys; + +=head NAME t::lib::Page::Opac::LeftNavigation + +=head SYNOPSIS + +Provides the services of the Opac left navigation column/frame for the implementing PageObject + +=cut + +################################################################################ +=head UI Mapping helper subroutines +See. Selenium documentation best practices for UI element mapping to common language descriptions. +=cut +################################################################################ + +=head _getLeftNavigationActionElements +@RETURNS HASHRef of Selenium::Driver::Webelements matching all the clickable elements + in the left navigation frame/column at all Opac pages requiring login. +=cut + +sub _getLeftNavigationActionElements { + my ($self) = @_; + my $d = $self->getDriver(); + + my $e = {}; + eval { + $e->{yourSummary} = $d->find_element("a[href*='opac-user.pl']", 'css'); + }; + eval { + $e->{yourFines} = $d->find_element("a[href*='opac-account.pl']", 'css'); + }; + eval { + $e->{yourPersonalDetails} = $d->find_element("a[href*='opac-memberentry.pl']", 'css'); + }; + eval { + $e->{yourTags} = $d->find_element("a[href*='opac-tags.pl']", 'css'); + }; + eval { + $e->{changeYourPassword} = $d->find_element("a[href*='opac-passwd.pl']", 'css'); + }; + eval { + $e->{yourSearchHistory} = $d->find_element("a[href*='opac-search-history.pl']", 'css'); + }; + eval { + $e->{yourReadingHistory} = $d->find_element("a[href*='opac-readingrecord.pl']", 'css'); + }; + eval { + $e->{yourPurchaseSuggestions} = $d->find_element("a[href*='opac-suggestions.pl']", 'css'); + }; + eval { + $e->{yourLists} = $d->find_element("a[href*='opac-shelves.pl']", 'css'); + }; + eval { + $e->{yourAPIKeys} = $d->find_element("a[href*='opac-apikeys.pl']", 'css'); + }; + return $e; +} + + + +################################################################################ +=head PageObject Services + +=cut +################################################################################ + +sub navigateYourAPIKeys { + my ($self) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my $elements = $self->_getLeftNavigationActionElements(); + $elements->{yourAPIKeys}->click(); + $self->debugTakeSessionSnapshot(); + + my $breadcrumbs = $self->_getBreadcrumbLinks(); + + ok(ref($breadcrumbs) eq 'ARRAY' && + $breadcrumbs->[scalar(@$breadcrumbs)-1]->get_text() =~ m/API keys/i, + "Opac Navigate to Your API Keys"); + + return t::lib::Page::Opac::OpacApiKeys->rebrandFromPageObject($self); +} + +1; #Make the compiler happy! diff --git a/t/lib/Page/Opac/OpacApiKeys.pm b/t/lib/Page/Opac/OpacApiKeys.pm new file mode 100644 index 0000000..1483094 --- /dev/null +++ b/t/lib/Page/Opac/OpacApiKeys.pm @@ -0,0 +1,170 @@ +package t::lib::Page::Opac::OpacApiKeys; + +# Copyright 2015 KohaSuomi! +# +# 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 Scalar::Util qw(blessed); +use Test::More; + +use base qw(t::lib::Page::Opac t::lib::Page::Opac::LeftNavigation); + +use Koha::Exception::BadParameter; +use Koha::Exception::UnknownObject; + +=head NAME t::lib::Page::Members::ApiKeys + +=head SYNOPSIS + +apikeys.pl PageObject providing page functionality as a service! + +=cut + +sub new { + Koha::Exception::FeatureUnavailable->throw(error => __PACKAGE__."->new():> You must login first to navigate to this page!"); +} + +################################################################################ +=head UI Mapping helper subroutines +See. Selenium documentation best practices for UI element mapping to common language descriptions. +=cut +################################################################################ + +=head _getActionsAndTableElements +@RETURNS List of + HASHRef of Selenium::Driver::Webelement-objects matching the generic + actions on this page, eg. 'generateNewKey'. + HASHRef of Selenium::Driver::Webelement-objects keyed with the apiKey hash/text. + These are all the apiKey table rows present, and have the + elements prefetched for easy access. + +=cut + +sub _getActionsAndTableElements { + my ($self) = @_; + my $d = $self->getDriver(); + + my $generateNewKeySubmit = $d->find_element("#generatenewkey", 'css'); + + my $a = {}; #Collect action elements here + $a->{generateNewKey} = $generateNewKeySubmit; #Bind the global action here for easy reference. + + my $apiKeyRows; + eval { #We might not have ApiKeys yet. + $apiKeyRows = $d->find_elements("#apikeystable tr", 'css'); + shift @$apiKeyRows; #Remove the table header row + }; + my %apiKeys; + for(my $i=0 ; $i[$i]; + $row->{'nth-of-type'} = $i+1; #starts from 1 + $row->{key} = $d->find_child_element($row, "td.apikeykey", 'css')->get_text(); + $row->{active} = $d->find_child_element($row, "td.apikeyactive", 'css')->get_text(); + $row->{lastTransaction} = $d->find_child_element($row, "td.apikeylastransaction", 'css')->get_text(); + $row->{delete} = $d->find_child_element($row, "input.apikeydelete", 'css'); + eval { + $row->{revoke} = $d->find_child_element($row, "input.apikeyrevoke", 'css'); + }; + eval { + $row->{activate} = $d->find_child_element($row, "input.apikeyactivate", 'css'); + }; + $apiKeys{$row->{key}} = $row; + } + + return ($a, \%apiKeys); +} + + + +################################################################################ +=head PageObject Services + +=cut +################################################################################ + +sub generateNewApiKey { + my ($self) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPre = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + $actionElements->{generateNewKey}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPost = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + is($apiKeyRowsCountPre+1, $apiKeyRowsCountPost, "ApiKey generated"); + return $self; +} + +sub revokeApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{revoke}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found after revoking it.") unless $apiKeyRow; + is($apiKeyRow->{active}, 'No', "ApiKey revoked"); + return $self; +} + +sub activateApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{activate}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found after activating it.") unless $apiKeyRow; + is($apiKeyRow->{active}, 'Yes', "ApiKey activated"); + return $self; +} + +sub deleteApiKey { + my ($self, $apiKey) = @_; + my $d = $self->getDriver(); + $self->debugTakeSessionSnapshot(); + + my ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPre = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + my $apiKeyRow = $apiKeyRows->{$apiKey}; + Koha::Exception::UnknownObject->throw(error => __PACKAGE__."revokeApiKey():> No matching apiKey '$apiKey' found.") unless $apiKeyRow; + $apiKeyRow->{delete}->click(); + $self->debugTakeSessionSnapshot(); + + ($actionElements, $apiKeyRows) = $self->_getActionsAndTableElements(); + my $apiKeyRowsCountPost = (ref $apiKeyRows eq 'HASH') ? scalar(keys(%$apiKeyRows)) : 0; + is($apiKeyRowsCountPre-1, $apiKeyRowsCountPost, "ApiKey deleted"); + return $self; +} + +1; #Make the compiler happy! diff --git a/t/lib/Page/Opac/OpacSearchHistory.pm b/t/lib/Page/Opac/OpacSearchHistory.pm index 70e6383..136d889 100644 --- a/t/lib/Page/Opac/OpacSearchHistory.pm +++ b/t/lib/Page/Opac/OpacSearchHistory.pm @@ -20,7 +20,7 @@ package t::lib::Page::Opac::OpacSearchHistory; use Modern::Perl; use Test::More; -use base qw(t::lib::Page::Opac); +use base qw(t::lib::Page::Opac t::lib::Page::Opac::LeftNavigation); use Koha::Exception::FeatureUnavailable; diff --git a/t/lib/Page/Opac/OpacUser.pm b/t/lib/Page/Opac/OpacUser.pm index 975750a..38fda04 100644 --- a/t/lib/Page/Opac/OpacUser.pm +++ b/t/lib/Page/Opac/OpacUser.pm @@ -19,7 +19,7 @@ package t::lib::Page::Opac::OpacUser; use Modern::Perl; -use base qw(t::lib::Page::Opac); +use base qw(t::lib::Page::Opac t::lib::Page::Opac::LeftNavigation); use Koha::Exception::FeatureUnavailable; @@ -61,4 +61,4 @@ See. Selenium documentation best practices for UI element mapping to common lang -1; #Make the compiler happy! \ No newline at end of file +1; #Make the compiler happy! -- 1.9.1