Bugzilla – Attachment 41321 Details for
Bug 13799
Add base for building RESTful API
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 13799: 4. API Authentication, part 1: API keys management in interface
Bug-13799-4-API-Authentication-part-1-API-keys-man.patch (text/plain), 58.90 KB, created by
Olli-Antti Kivilahti
on 2015-08-03 16:09:43 UTC
(
hide
)
Description:
Bug 13799: 4. API Authentication, part 1: API keys management in interface
Filename:
MIME Type:
Creator:
Olli-Antti Kivilahti
Created:
2015-08-03 16:09:43 UTC
Size:
58.90 KB
patch
obsolete
>From a21edfc558557398b68d6c2ec58021b1f6716065 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Mon, 23 Mar 2015 20:14:23 +0100 >Subject: [PATCH] Bug 13799: 4. API Authentication, part 1: API keys management > in interface > >Depends on Buugg 14539 and Buugg 7174. > >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 | 15 -- > 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, 1452 insertions(+), 21 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<scalar(@dbix_objects) ; $i++) { >+ $dbix_objects[$i] = Koha::ApiKey->_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 <kyle@bywatersolutions.com> >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<api_keys> >+ >+=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</api_key_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("api_key_id"); >+ >+=head1 UNIQUE CONSTRAINTS >+ >+=head2 C<apk_bornumkey_idx> >+ >+=over 4 >+ >+=item * L</borrowernumber> >+ >+=item * L</api_key> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->add_unique_constraint("apk_bornumkey_idx", ["borrowernumber", "api_key"]); >+ >+=head1 RELATIONS >+ >+=head2 borrowernumber >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Borrower> >+ >+=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 5356dfd..cad98d1 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<Koha::Schema::Result::ApiTimestamp> >- >-=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 >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 7392c2f..541313e 100755 >--- a/installer/data/mysql/updatedatabase.pl >+++ b/installer/data/mysql/updatedatabase.pl >@@ -10733,6 +10733,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({module => 'borrowers', 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 %] > <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to set permissions" id="patronflags" href="#">Set permissions</a></li> > [% END %] >+ [% IF ( CAN_user_borrowers_manage_api_keys ) %] >+ <li><a id="apikeys" href="/cgi-bin/koha/members/apikeys.pl?borrowernumber=[% borrowernumber %]">Manage API keys</a></li> >+ [% ELSE %] >+ <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to manage API keys" id="apikeys" href="#">Manage API keys</a></li> >+ [% END %] > [% IF ( CAN_user_borrowers ) %] > [% IF ( NorwegianPatronDBEnable == 1 ) %] > <li><a id="deletepatronlocal" href="#">Delete local</a></li> >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' %] >+<title>Koha › Patrons › API Keys</title> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+<body id="pat_apikeys" class="pat"> >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'patron-search.inc' %] >+ >+<div id="breadcrumbs"> >+ <a href="/cgi-bin/koha/mainpage.pl">Home</a> >+ › >+ <a href="/cgi-bin/koha/members/members-home.pl">Patrons</a> >+ › >+ API Keys for [% INCLUDE 'patron-title.inc' %] >+</div> >+ >+<div id="doc3" class="yui-t2"> >+ <div id="bd"> >+ <div id="yui-main"> >+ <div class="yui-b"> >+ [% INCLUDE 'members-toolbar.inc' %] >+ >+ <h1>API keys for [% INCLUDE 'patron-title.inc' %]</h1> >+ <div> >+ <form action="/cgi-bin/koha/members/apikeys.pl" method="post"> >+ <input type="hidden" name="borrowernumber" value="[% borrowernumber %]"> >+ <input type="hidden" name="op" value="generate"> >+ <input id="generatenewkey" type="submit" value="Generate new key"> >+ </form> >+ </div> >+ [% IF api_keys.size > 0 %] >+ <table id="apikeystable"> >+ <thead> >+ <tr> >+ <th>Key</th> >+ <th>Active</th> >+ <th>Last transaction</th> >+ <th>Actions</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH key IN api_keys %] >+ <tr> >+ <td class="apikeykey">[% key.api_key %]</td> >+ <td class="apikeyactive">[% IF key.active %]Yes[% ELSE %]No[% END %]</td> >+ <td class="apikeylastransaction">[% key.last_request_time || '' %]</td> >+ <td> >+ <form action="/cgi-bin/koha/members/apikeys.pl" method="post"> >+ <input type="hidden" name="borrowernumber" value="[% borrowernumber %]"> >+ <input type="hidden" name="key" value="[% key.api_key %]"> >+ <input type="hidden" name="op" value="delete"> >+ <input class="apikeydelete" type="submit" value="Delete"> >+ </form> >+ <form action="/cgi-bin/koha/members/apikeys.pl" method="post"> >+ <input type="hidden" name="borrowernumber" value="[% borrowernumber %]"> >+ <input type="hidden" name="key" value="[% key.api_key %]"> >+ [% IF key.active %] >+ <input type="hidden" name="op" value="revoke"> >+ <input class="apikeyrevoke" type="submit" value="Revoke"> >+ [% ELSE %] >+ <input type="hidden" name="op" value="activate"> >+ <input class="apikeyactivate" type="submit" value="Activate"> >+ [% END %] >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ </div> >+ </div> >+ <div class="yui-b"> >+ [% INCLUDE 'circ-menu.inc' %] >+ </div> >+ </div> >+[% 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 ) %] > <div id="menu"> > <h4><a href="#" class="menu-collapse-toggle">Your account menu</a></h4> >@@ -102,6 +103,13 @@ > [% END %] > <a href="/cgi-bin/koha/opac-discharge.pl">ask for a discharge</a></li> > [% END %] >+ >+ [% IF apikeysview %] >+ <li class="active"> >+ [% ELSE %] >+ <li> >+ [% END %] >+ <a href="/cgi-bin/koha/opac-apikeys.pl">your API keys</a></li> > </ul> > </div> > [% 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' %] >+<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog › Your library home › Your API keys</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% BLOCK cssinclude %][% END %] >+</head> >+[% INCLUDE 'bodytag.inc' bodyid='opac-user' bodyclass='scrollto' %] >+[% INCLUDE 'masthead.inc' %] >+ >+<div class="main"> >+ <ul class="breadcrumb"> >+ <li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">›</span></li> >+ <li> >+ <a href="/cgi-bin/koha/opac-user.pl"> >+ [% INCLUDE 'patron-title.inc' category_type = borrower.category_type firstname = borrower.firstname surname = borrower.surname othernames = borrower.othernames %] >+ </a> >+ <span class="divider">›</span> >+ </li> >+ <li><a href="/cgi-bin/koha/opac-apikeys.pl">Your API keys</a></li> >+ </ul> >+ >+ <div class="container-fluid"> >+ <div class="row-fluid"> >+ <div class="span2"> >+ <div id="navigation"> >+ [% INCLUDE 'navigation.inc' IsPatronPage = 1 %] >+ </div> >+ </div> >+ <div class="span10"> >+ <div id="apikeys" class="maincontent"> >+ <h1>Your API keys</h1> >+ <div> >+ <form action="/cgi-bin/koha/opac-apikeys.pl" method="post"> >+ <input type="hidden" name="op" value="generate"> >+ <input id="generatenewkey" type="submit" value="Generate new key"> >+ </form> >+ </div> >+ [% IF api_keys.size > 0 %] >+ <table id="apikeystable" class="table table-bordered table-striped"> >+ <thead> >+ <tr> >+ <th>Key</th> >+ <th>Active</th> >+ <th>Last transaction</th> >+ <th>Actions</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH key IN api_keys %] >+ <tr> >+ <td class="apikeykey">[% key.api_key %]</td> >+ <td class="apikeyactive">[% IF key.active %]Yes[% ELSE %]No[% END %]</td> >+ <td class="apikeylastransaction">[% key.last_request_time || '' %]</td> >+ <td> >+ <form action="/cgi-bin/koha/opac-apikeys.pl" method="post" class="form-inline"> >+ <input type="hidden" name="key" value="[% key.api_key %]"> >+ <input type="hidden" name="op" value="delete"> >+ <input class="apikeydelete" type="submit" value="Delete"> >+ </form> >+ <form action="/cgi-bin/koha/opac-apikeys.pl" method="post" class="form-inline"> >+ <input type="hidden" name="key" value="[% key.api_key %]"> >+ [% IF key.active %] >+ <input type="hidden" name="op" value="revoke"> >+ <input class="apikeyrevoke" type="submit" value="Revoke"> >+ [% ELSE %] >+ <input type="hidden" name="op" value="activate"> >+ <input class="apikeyactivate" type="submit" value="Activate"> >+ [% END %] >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ </div> <!-- /#apikeys --> >+ </div> <!-- /.span10 --> >+ </div> <!-- /.row-fluid --> >+ </div> <!-- /.container-fluid --> >+</div> <!-- /#main --> >+ >+[% 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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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<scalar(@$apiKeyRows) ; $i++) { >+ #Iterate every apiKey in the apiKeys table and prefetch the interesting data as text and available action elements. >+ my $row = $apiKeyRows->[$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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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<scalar(@$apiKeyRows) ; $i++) { >+ #Iterate every apiKey in the apiKeys table and prefetch the interesting data as text and available action elements. >+ my $row = $apiKeyRows->[$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
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 13799
:
36700
|
36705
|
36938
|
36941
|
37119
|
37132
|
37172
|
38246
|
39156
|
39158
|
39159
|
39160
|
39161
|
39162
|
39163
|
39164
|
40088
|
40092
|
40093
|
40094
|
40098
|
40099
|
40101
|
40140
|
40141
|
40142
|
40463
|
40464
|
40465
|
40466
|
40467
|
40468
|
40469
|
40470
|
40471
|
40551
|
40552
|
40553
|
40554
|
40560
|
40561
|
40584
|
40585
|
40586
|
40587
|
40616
|
40617
|
40618
|
40619
|
41160
|
41161
|
41162
|
41268
|
41269
|
41270
|
41271
|
41272
|
41273
|
41274
|
41320
|
41321
|
41326
|
41327
|
41328
|
41329
|
41330
|
41331
|
41538
|
41539
|
41540
|
41541
|
41542
|
41543
|
41547
|
41554
|
42044
|
42085
|
42086
|
42123
|
42518
|
42766
|
42768
|
42801
|
42874
|
42875
|
42876
|
42877
|
42878
|
43186
|
43187
|
43189
|
43190
|
43191
|
43192
|
43204
|
43205
|
43206
|
43207
|
43208
|
43209
|
43210
|
43211
|
43212
|
43213
|
43214
|
43980
|
43981
|
43987
|
43989
|
43990
|
43991
|
43992
|
43993
|
43994
|
43995
|
43996
|
43997
|
43998
|
43999
|
44000
|
44001
|
44185
|
44285
|
44313
|
44375
|
44376
|
44377
|
44378
|
44379
|
44380
|
44381
|
44382
|
44383
|
44384
|
44385