summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBill MacAllister <whm@stanford.edu>2016-08-14 00:56:44 -0700
committerBill MacAllister <whm@stanford.edu>2016-08-14 00:56:44 -0700
commit18225a1b837a0132ffbbf428b0516dfd949fdb98 (patch)
treedb2422d95c38d7e0cc9c1fe7cdcdb692d95c040a
parent31b242637975e2c237983d3c97c01693cce4a608 (diff)
parent58f98fcb42c5b48c1686839f955a304bf2b844ca (diff)
Merge tag 'upstream/3.0.4'
Upstream version 3.0.4
-rw-r--r--.gitignore24
-rw-r--r--Changes17
-rw-r--r--Credits6
-rw-r--r--LDAPapi.pm243
-rw-r--r--LDAPapi.xs293
-rw-r--r--MANIFEST27
-rw-r--r--META.yml11
-rw-r--r--Makefile.PL10
-rw-r--r--README13
-rw-r--r--README.md1
-rwxr-xr-xexamples/ldapwalk-dieter.pl176
-rwxr-xr-xexamples/ldapwalk.pl10
-rwxr-xr-xexamples/ldapwalk2.pl2
-rwxr-xr-xexamples/testurl.pl83
-rw-r--r--t/01-bdd-cucumber.t37
-rw-r--r--t/features/add.feature24
-rw-r--r--t/features/bind.feature40
-rw-r--r--t/features/compare.feature29
-rw-r--r--t/features/delete.feature29
-rw-r--r--t/features/extended_operations.feature51
-rw-r--r--t/features/modify.feature83
-rw-r--r--t/features/options.feature12
-rw-r--r--t/features/rename.feature34
-rw-r--r--t/features/search.feature77
-rw-r--r--t/features/server_controls.feature21
-rw-r--r--t/features/step_definitions/add_steps.pl35
-rw-r--r--t/features/step_definitions/bind_steps.pl53
-rw-r--r--t/features/step_definitions/compare_steps.pl36
-rw-r--r--t/features/step_definitions/delete_steps.pl34
-rw-r--r--t/features/step_definitions/extended_operation_steps.pl59
-rw-r--r--t/features/step_definitions/general_steps.pl104
-rw-r--r--t/features/step_definitions/modify_steps.pl76
-rw-r--r--t/features/step_definitions/options_steps.pl33
-rw-r--r--t/features/step_definitions/rename_steps.pl35
-rw-r--r--t/features/step_definitions/search_steps.pl105
-rw-r--r--t/features/step_definitions/server_controls_steps.pl136
-rw-r--r--t/features/step_definitions/syncrepl_steps.pl81
-rw-r--r--t/features/step_definitions/whoami_steps.pl65
-rw-r--r--t/features/syncrepl.feature21
-rw-r--r--t/features/whoami.feature61
-rw-r--r--t/test-config.pl129
-rw-r--r--test.pl32
-rw-r--r--typemap1
43 files changed, 2384 insertions, 65 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ad560f7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+blib/
+/.build/
+_build/
+cover_db/
+inc/
+Build
+!Build/
+Build.bat
+.last_cover_stats
+Makefile
+Makefile.old
+MANIFEST.bak
+META.json
+MYMETA.*
+nytprof.out
+pm_to_blib
+*.o
+*.bs
+*.gcov
+*.gcda
+*.gcno
+*~
+LDAPapi.c
+constant.h
diff --git a/Changes b/Changes
index ea4adfb..7ba0723 100644
--- a/Changes
+++ b/Changes
@@ -1,4 +1,21 @@
Revision history for Perl5 Net::LDAPapi Module.
+3.0.4 Mon Nov 30 19:00:00 PST 2015
+ - Fix undef comparison
+ - Misc variable initializations to quiet warnings
+ - Fixed sasl mechanisms initializtion
+ - Examples cleanup
+ - LDAPv3 extended operation support
+ - New developer mode test suite
+ - Fixed quanah/net-ldapapi#3: ldap_set_rebind_proc XS being called with invalid arguments from set_rebind_proc
+ - Fixed quanah/net-ldapapi#6: ldap_sasl_bind has wrong prototype in LDAPapi.xs
+ - Fixed quanah/net-ldapapi#8: search_s() clobbers ATTRS parameter
+ - Fixed quanah/net-ldapapi#11: result() blocking when called with output from rename()
+ - Fixed quanah/net-ldapapi#20: ldap_result() doesn't honour passed timeout value
+ - Fixed quanah/net-ldapapi#21: ldap_set_option(LDAP_OPT_TIMEOUT, 1) on OpenLDAP returns -1
+ - Fixed quanah/net-ldapapi#28: Server control responses get eaten after a NULL character in the berval
+ - Fixed quanah/net-ldapapi#30: ldap_search_ext() and ldap_search_ext_s() segfault when used with timeout
+ - Fixed quanah/net-ldapapi#31: ldap_result() and ldap_url_search_st() timeout parameters have a granularity of 1 second
+ - Fixed quanah/net-ldapapi#40: Server control requests get eaten after a NULL character in the berval
3.0.3 Wed Aug 20 12:23:00 PST 2008
- Add Convert::ASN1 requirement.
- Fix error code handling (CPAN bug#35910)
diff --git a/Credits b/Credits
index 309889a..4d6bfd9 100644
--- a/Credits
+++ b/Credits
@@ -31,9 +31,13 @@ Howard Chu <hyc@symas.com>
- For getting this project started again
Marcus Watts
- For submitting patches to the new code line
+Phillip O'Donnell <podonnell@cpan.org>
+ - Extended operation support
+ - Behaviour driven development test suite
+ - Bugfixes
--
Quanah Gibson-Mount
-mishikal@yahoo.com
+quanah.gibsonmount@gmail.com
CPAN: /by-authors/id/M/MI/MISHIKAL/
--
Clayton Donley
diff --git a/LDAPapi.pm b/LDAPapi.pm
index 16245f3..6e01fb7 100644
--- a/LDAPapi.pm
+++ b/LDAPapi.pm
@@ -4,6 +4,7 @@ use strict;
use Carp;
use Convert::ASN1;
use vars qw($VERSION @ISA @EXPORT $AUTOLOAD);
+no warnings "uninitialized";
require Exporter;
require DynaLoader;
@@ -21,6 +22,8 @@ require AutoLoader;
ldap_rename ldap_rename_s
ldap_compare_ext ldap_compare_ext_s ldap_delete_ext
ldap_delete_ext_s ldap_search_ext ldap_search_ext_s ldap_result
+ ldap_extended_operation ldap_extended_operation_s ldap_parse_extended_result
+ ldap_parse_whoami ldap_whoami ldap_whoami_s
ldap_msgfree ldap_msg_free ldap_msgid ldap_msgtype
ldap_get_lderrno ldap_set_lderrno ldap_parse_result ldap_err2string
ldap_count_entries ldap_first_entry ldap_next_entry ldap_get_dn
@@ -159,6 +162,8 @@ require AutoLoader;
LDAP_OPT_SSL
LDAP_OPT_THREAD_FN_PTRS
LDAP_OPT_TIMELIMIT
+ LDAP_OPT_TIMEOUT
+ LDAP_OPT_NETWORK_TIMEOUT
LDAP_OTHER
LDAP_PARAM_ERROR
LDAP_PARTIAL_RESULTS
@@ -227,7 +232,7 @@ require AutoLoader;
LDAP_TAG_IM_RES_VALUE
LDAP_TAG_SASL_RES_CREDS
);
-$VERSION = '3.0.3';
+$VERSION = '3.0.4';
sub AUTOLOAD {
# This AUTOLOAD is used to 'autoload' constants from the constant()
@@ -482,7 +487,7 @@ sub bind_s
{
my ($self, @args) = @_;
- my ($saslmech, $status, $servercredp, $sctrls, $cctrls);
+ my ($status, $servercredp, $sctrls, $cctrls);
my ($dn, $pass, $authtype, $serverctrls, $clientctrls) =
$self->rearrange(['DN', 'PASSWORD', 'TYPE', 'SCTRLS', 'CCTRLS'], @args);
@@ -499,7 +504,7 @@ sub bind_s
if ($authtype == $self->LDAP_AUTH_SASL) {
$status =
ldap_sasl_interactive_bind_s($self->{"ld"}, $dn, $pass,
- $sctrls, $cctrls, $saslmech,
+ $sctrls, $cctrls, $self->{"saslmech"},
$self->{"saslrealm"},
$self->{"saslauthzid"},
$self->{"saslsecprops"},
@@ -837,6 +842,10 @@ sub next_changed_entries {
@entries = ();
+ if ($self->{'status'} == 0) { # ldap_result return 0 = timeout
+ return @entries;
+ }
+
$asn = $self->{"asn"};
while( $msg = $self->result_message ) {
@@ -1114,6 +1123,31 @@ sub parse_result {
return %result;
} # end of parse_result(...)
+sub parse_extended_result {
+ my ($self, @args) = @_;
+
+ my ($msg, $freeMsg) = $self->rearrange(['MSG', 'FREEMSG'], @args);
+
+ my ($status, %result);
+
+ $freeMsg = 0 unless $freeMsg;
+ $msg = $self->{"msg"} unless $msg;
+
+ my ($retoidp, $retdatap);
+
+ $status =
+ ldap_parse_extended_result($self->{"ld"}, $msg, $retoidp, $retdatap, $freeMsg);
+
+ $self->errorize($status);
+ if( $status != $self->LDAP_SUCCESS ) {
+ return undef;
+ }
+
+ $result{"retoidp"} = $retoidp;
+ $result{"retdatap"} = $retdatap;
+
+ return %result;
+} # end of parse_extended_result(...)
# needs docs bellow in POD. XXX
sub parse_intermediate {
@@ -1147,6 +1181,27 @@ sub parse_intermediate {
return %result;
} # end of parse_result(...)
+sub parse_whoami {
+ my ($self, @args) = @_;
+
+ my ($msg) = $self->rearrange(['MSG'], @args);
+
+ my ($status, %result);
+
+ $msg = $self->{"msg"} unless $msg;
+
+ my ($authzid);
+
+ $status =
+ ldap_parse_whoami($self->{"ld"}, $msg, $authzid);
+
+ $self->errorize($status);
+ if( $status != $self->LDAP_SUCCESS ) {
+ return undef;
+ }
+
+ return $authzid;
+} # end of parse_whoami(...)
sub perror
{
@@ -1478,12 +1533,12 @@ sub listen_for_changes
$self->rearrange(['BASEDN', 'SCOPE', 'FILTER', 'ATTRS',
'ATTRSONLY', 'TIMEOUT', 'SIZELIMIT', 'COOKIE'], @args);
- croak("No Filter Specified") if ($filter eq "");
+ croak("No Filter Specified") if (!defined($filter));
croak("No cookie file specified") unless $cookie;
$self->{"cookie"} = $cookie;
- if( $attrs == undef ) {
+ if( !defined($attrs) ) {
my @null_array = ();
$attrs = \@null_array;
}
@@ -1542,9 +1597,9 @@ sub search
'SIZELIMIT'],
@args);
- croak("No Filter Specified") if ($filter eq "");
+ croak("No Filter Specified") if (!defined($filter));
- if( $attrs == undef ) {
+ if( !defined($attrs) ) {
my @null_array = ();
$attrs = \@null_array;
}
@@ -1592,7 +1647,7 @@ sub search_s
croak("No Filter Passed as Argument 3") if ($filter eq "");
- if( $attrs == undef ) {
+ if( !defined($attrs) ) {
my @null_array = ();
$attrs = \@null_array;
}
@@ -1623,6 +1678,110 @@ sub search_ext_s
return $self->search_s(@args);
} # end of search_ext_s
+sub extended_operation
+{
+ my ($self, @args) = @_;
+ my ($msgid, $status, $sctrls, $cctrls);
+
+ my ($oid, $berval, $serverctrls, $clientctrls) =
+ $self->rearrange(['OID', 'BERVAL',
+ 'SCTRLS', 'CCTRLS'],
+ @args);
+
+ $sctrls = $self->create_controls_array(@$serverctrls) if $serverctrls;
+ $cctrls = $self->create_controls_array(@$clientctrls) if $clientctrls;
+
+ $status = ldap_extended_operation($self->{"ld"}, $oid, $berval, length($berval),
+ $sctrls, $cctrls,
+ $msgid);
+
+ ldap_controls_array_free($sctrls) if $sctrls;
+ ldap_controls_array_free($cctrls) if $cctrls;
+
+ $self->errorize($status);
+ if( $status != $self->LDAP_SUCCESS ) {
+ return undef;
+ }
+
+ return $msgid;
+} # end of extended_operation
+
+sub extended_operation_s
+{
+ my ($self, @args) = @_;
+ my ($status, $retoidp, $retdatap, $sctrls, $cctrls);
+
+ my ($oid, $berval, $serverctrls, $clientctrls, $result) =
+ $self->rearrange(['OID', 'BERVAL',
+ 'SCTRLS', 'CCTRLS', 'RESULT'],
+ @args);
+
+ $sctrls = $self->create_controls_array(@$serverctrls) if $serverctrls;
+ $cctrls = $self->create_controls_array(@$clientctrls) if $clientctrls;
+
+ $status = ldap_extended_operation_s($self->{"ld"}, $oid, $berval, length($berval),
+ $sctrls, $cctrls,
+ $retoidp, $retdatap);
+
+ ldap_controls_array_free($sctrls) if $sctrls;
+ ldap_controls_array_free($cctrls) if $cctrls;
+
+ $self->errorize($status);
+
+ $result->{'retoidp'} = $retoidp;
+ $result->{'retdatap'} = $retdatap;
+
+ return $status;
+} # end of extended_operation_s
+
+sub whoami
+{
+ my ($self, @args) = @_;
+ my ($msgid, $status, $sctrls, $cctrls);
+
+ my ($serverctrls, $clientctrls) =
+ $self->rearrange(['SCTRLS', 'CCTRLS'],
+ @args);
+
+ $sctrls = $self->create_controls_array(@$serverctrls) if $serverctrls;
+ $cctrls = $self->create_controls_array(@$clientctrls) if $clientctrls;
+
+ $status = ldap_whoami($self->{"ld"}, $sctrls, $cctrls, $msgid);
+
+ ldap_controls_array_free($sctrls) if $sctrls;
+ ldap_controls_array_free($cctrls) if $cctrls;
+
+ $self->errorize($status);
+ if( $status != $self->LDAP_SUCCESS ) {
+ return undef;
+ }
+
+ return $msgid;
+} # end of whoami
+
+sub whoami_s
+{
+ my ($self, @args) = @_;
+ my ($status, $authzidOut, $sctrls, $cctrls);
+
+ my ($authzid, $serverctrls, $clientctrls) =
+ $self->rearrange(['AUTHZID', 'SCTRLS', 'CCTRLS'],
+ @args);
+
+ $sctrls = $self->create_controls_array(@$serverctrls) if $serverctrls;
+ $cctrls = $self->create_controls_array(@$clientctrls) if $clientctrls;
+
+ $status = ldap_whoami_s($self->{"ld"}, $authzidOut, $sctrls, $cctrls);
+
+ ldap_controls_array_free($sctrls) if $sctrls;
+ ldap_controls_array_free($cctrls) if $cctrls;
+
+ $self->errorize($status);
+
+ $$authzid = $authzidOut;
+
+ return $status;
+} # end of whoami_s
sub count_references
{
@@ -1668,10 +1827,10 @@ sub set_rebind_proc
my ($self, @args) = @_;
my ($status);
- my ($rebindproc) = $self->rearrange(['REBINDPROC'], @args);
+ my ($rebindproc, $params) = $self->rearrange(['REBINDPROC', 'PARAMS'], @args);
if( ref($rebindproc) eq "CODE" ) {
- $status = ldap_set_rebind_proc($self->{"ld"}, $rebindproc);
+ $status = ldap_set_rebind_proc($self->{"ld"}, $rebindproc, $params);
} else {
croak("REBINDPROC is not a CODE Reference");
}
@@ -1877,7 +2036,7 @@ sub create_control
croak("No OID of controls is passed") unless $oid;
croak("No BerVal is passed") unless $berval;
- $critical = 1 if $critical == undef;
+ $critical = 1 if !defined($critical);
my ($ctrl) = undef;
my $status = ldap_create_control($oid, $berval, length($berval), $critical, $ctrl);
@@ -2287,7 +2446,26 @@ Net::LDAPapi - Perl5 Module Supporting LDAP API
return($dn,$pass,LDAP_AUTH_SIMPLE);
}
-
+=head1 EXTENDED OPERATIONS
+
+ Extended operations are supported.
+
+ The extended_operation and extended_operation_s methods are used to
+ invoke extended operations.
+
+ Example (WHOAMI):
+
+ %result = ();
+
+ if ($ld->extended_operation_s(-oid => "1.3.6.1.4.1.4203.1.11.3", -result => \%result) != LDAP_SUCCESS)
+ {
+ $ld->perror("ldap_extended_operation_s");
+ exit -1;
+ }
+
+ Note that WHOAMI is already natively implemented via whoami and whoami_s
+ methods.
+
=head1 SUPPORTED METHODS
=over 4
@@ -2455,6 +2633,27 @@ Net::LDAPapi - Perl5 Module Supporting LDAP API
@components = $ld->explode_rdn($rdn, 0);
+=item extended_operation OID BERVAL SCTRLS CCTRLS
+
+ Asynchronous method for invoking an extended operation.
+
+ Returns a non-negative MSGID upon success.
+
+ Examples:
+
+ $msgid = $ld->extended_operation("1.3.6.1.4.1.4203.1.11.3");
+
+=item extended_operation_s OID BERVAL SCTRLS CCTRLS RESULT
+
+ Synchronous method for invoking an extended operation.
+
+ Returns LDAP_SUCCESS upon success.
+
+ Examples:
+
+ $status = $ld->extended_operation_s(-oid => "1.3.6.1.4.1.4203.1.11.3", \
+ -result => \%result);
+
=item first_attribute
Returns pointer to first attribute name found in the current entry.
@@ -2907,6 +3106,26 @@ Net::LDAPapi - Perl5 Module Supporting LDAP API
$status = $ld->url_search_s($my_ldap_url,0,2);
+=item whoami SCTRLS CCTRLS
+
+ Asynchronous method for invoking an LDAP whoami extended operation.
+
+ Returns a non-negative MSGID upon success.
+
+ Examples:
+
+ $msgid = $ld->whoami();
+
+=item whoami_s AUTHZID SCTRLS CCTRLS
+
+ Synchronous method for invoking an LDAP whoami extended operation.
+
+ Returns LDAP_SUCCESS upon success.
+
+ Examples:
+
+ $status = $ld->whoami_s(\$authzid);
+
=back
=head1 AUTHOR
diff --git a/LDAPapi.xs b/LDAPapi.xs
index a40c641..9de1944 100644
--- a/LDAPapi.xs
+++ b/LDAPapi.xs
@@ -334,6 +334,32 @@ ldap_b2_interact(LDAP *ld, unsigned flags, void *def, void *inter)
return LDAP_SUCCESS;
}
+static struct timeval *
+sv2timeval(SV *data)
+{
+ struct timeval *tv = NULL;
+
+ if (SvPOK(data))
+ {
+ /* set the NV flag if it's readable as a double */
+ SvNV(data);
+ }
+
+ if (SvIOK(data) || SvNOK(data)) {
+ Newx(tv, 1, struct timeval);
+
+ tv->tv_sec = SvIV(data);
+ tv->tv_usec = ((SvNV(data) - SvIV(data))*1000000);
+ }
+
+ return tv;
+}
+
+static SV *
+timeval2sv(struct timeval *data)
+{
+ return newSVnv(data->tv_sec + ((double)data->tv_usec / 1000000));
+}
MODULE = Net::LDAPapi PACKAGE = Net::LDAPapi
@@ -384,10 +410,41 @@ int
ldap_set_option(ld,option,optdata)
LDAP * ld
int option
- int optdata
+ SV * optdata
CODE:
{
- RETVAL = ldap_set_option(ld,option,&optdata);
+ void *optptr = NULL;
+
+ bool must_safefree = 0;
+
+ int sv_i;
+
+ switch(option)
+ {
+#ifdef OPENLDAP
+ case LDAP_OPT_TIMEOUT:
+ case LDAP_OPT_NETWORK_TIMEOUT:
+ optptr = (void *) sv2timeval(optdata);
+ must_safefree = 1;
+
+ break;
+#endif
+ default:
+ if (SvIOK(optdata))
+ {
+ sv_i = SvIV(optdata);
+ optptr = (void *) &sv_i;
+ }
+
+ break;
+ }
+
+ RETVAL = ldap_set_option(ld,option,optptr);
+
+ if (must_safefree)
+ {
+ Safefree(optptr);
+ }
}
OUTPUT:
RETVAL
@@ -396,10 +453,26 @@ int
ldap_get_option(ld,option,optdata)
LDAP * ld
int option
- int optdata = NO_INIT
+ SV * optdata
CODE:
{
- RETVAL = ldap_get_option(ld, option, &optdata);
+ void *data = NULL;
+
+ RETVAL = ldap_get_option(ld, option, &data);
+
+ switch(option)
+ {
+#ifdef OPENLDAP
+ case LDAP_OPT_TIMEOUT:
+ case LDAP_OPT_NETWORK_TIMEOUT:
+ sv_setsv(SvRV(optdata), timeval2sv(data));
+ break;
+#endif
+ default:
+ sv_setiv(SvRV(optdata), (long)data);
+ break;
+ }
+
}
OUTPUT:
RETVAL
@@ -472,7 +545,7 @@ ldap_add_ext_s(ld,dn,ldap_change_ref,sctrls,cctrls)
Safefree(ldap_change_ref);
int
-ldap_sasl_bind(ld, dn, passwd, sctrls, serverctrls, clientctrls, msgidp)
+ldap_sasl_bind(ld, dn, passwd, serverctrls, clientctrls, msgidp)
LDAP * ld
LDAP_CHAR * dn
LDAP_CHAR * passwd
@@ -540,6 +613,7 @@ ldap_rename(ld, dn, newrdn, newSuperior, deleteoldrdn, sctrls, cctrls, msgidp)
}
OUTPUT:
RETVAL
+ msgidp
int
ldap_rename_s(ld, dn, newrdn, newSuperior, deleteoldrdn, sctrls, cctrls)
@@ -621,7 +695,7 @@ ldap_search_ext(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, timeo
int attrsonly
LDAPControl ** sctrls
LDAPControl ** cctrls
- struct timeval * timeout
+ SV * timeout
int sizelimit
int msgidp = NO_INIT
@@ -630,6 +704,7 @@ ldap_search_ext(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, timeo
char **attrs_char;
SV **current;
int arraylen,count;
+ struct timeval *tv_timeout = NULL;
if (SvTYPE(SvRV(attrs)) != SVt_PVAV)
{
@@ -650,9 +725,13 @@ ldap_search_ext(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, timeo
}
attrs_char[arraylen+1] = NULL;
}
+
+ tv_timeout = sv2timeval(timeout);
+
RETVAL = ldap_search_ext(ld, base, scope, filter, attrs_char,
- attrsonly, sctrls, cctrls, timeout, sizelimit,
+ attrsonly, sctrls, cctrls, tv_timeout, sizelimit,
&msgidp);
+ Safefree(tv_timeout);
Safefree(attrs_char);
}
OUTPUT:
@@ -669,7 +748,7 @@ ldap_search_ext_s(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, tim
int attrsonly
LDAPControl ** sctrls
LDAPControl ** cctrls
- struct timeval * timeout
+ SV * timeout
int sizelimit
LDAPMessage * res = NO_INIT
CODE:
@@ -677,6 +756,7 @@ ldap_search_ext_s(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, tim
char **attrs_char;
SV **current;
int arraylen,count;
+ struct timeval *tv_timeout = NULL;
if (SvTYPE(SvRV(attrs)) == SVt_PVAV)
{
@@ -697,31 +777,138 @@ ldap_search_ext_s(ld, base, scope, filter, attrs, attrsonly, sctrls, cctrls, tim
croak("Net::LDAPapi::ldap_search_ext_s needs ARRAY reference as argument 5.");
XSRETURN(1);
}
- RETVAL = ldap_search_ext_s(ld,base,scope,filter,attrs_char,attrsonly,sctrls,cctrls,timeout,sizelimit,&res);
+
+ tv_timeout = sv2timeval(timeout);
+
+ RETVAL = ldap_search_ext_s(ld,base,scope,filter,attrs_char,attrsonly,sctrls,cctrls,tv_timeout,sizelimit,&res);
+
+ Safefree(tv_timeout);
Safefree(attrs_char);
}
OUTPUT:
RETVAL
res
+int
+ldap_extended_operation(ld, oid, bv_val, bv_len, sctrls, cctrls, msgidp)
+ LDAP * ld
+ LDAP_CHAR * oid
+ LDAP_CHAR * bv_val
+ int bv_len
+ LDAPControl ** sctrls
+ LDAPControl ** cctrls
+ int msgidp = NO_INIT
+
+ CODE:
+ {
+ struct berval indata;
+
+ if (bv_len == 0) {
+ RETVAL = ldap_extended_operation(ld, oid, NULL,
+ sctrls, cctrls,
+ &msgidp);
+ } else {
+ indata.bv_val = bv_val;
+ indata.bv_len = bv_len;
+
+ RETVAL = ldap_extended_operation(ld, oid, &indata,
+ sctrls, cctrls,
+ &msgidp);
+ }
+ }
+ OUTPUT:
+ RETVAL
+ msgidp
+
+int
+ldap_extended_operation_s(ld, oid, bv_val, bv_len, sctrls, cctrls, retoidp, retdatap)
+ LDAP * ld
+ LDAP_CHAR * oid
+ LDAP_CHAR * bv_val
+ int bv_len
+ LDAPControl ** sctrls
+ LDAPControl ** cctrls
+ char * retoidp = NO_INIT
+ char * retdatap = NO_INIT
+ CODE:
+ {
+ struct berval indata, *retdata;
+
+ if (bv_len == 0) {
+ RETVAL = ldap_extended_operation_s(ld, oid, NULL,
+ sctrls, cctrls,
+ &retoidp, &retdata);
+ } else {
+ indata.bv_val = bv_val;
+ indata.bv_len = bv_len;
+
+ RETVAL = ldap_extended_operation_s(ld, oid, &indata,
+ sctrls, cctrls,
+ &retoidp, &retdata);
+ }
+
+ if (retdata != NULL)
+ retdatap = ldap_strdup(retdata->bv_val);
+
+ ber_memfree(retdata);
+ }
+ OUTPUT:
+ RETVAL
+ retoidp
+ retdatap
+
+int
+ldap_whoami(ld, sctrls, cctrls, msgidp)
+ LDAP * ld
+ LDAPControl ** sctrls
+ LDAPControl ** cctrls
+ int msgidp = NO_INIT
+
+ CODE:
+ {
+ RETVAL = ldap_whoami(ld, sctrls, cctrls,
+ &msgidp);
+ }
+ OUTPUT:
+ RETVAL
+ msgidp
+
+int
+ldap_whoami_s(ld, authzid, sctrls, cctrls)
+ LDAP * ld
+ LDAPControl ** sctrls
+ LDAPControl ** cctrls
+ char * authzid = NO_INIT
+ CODE:
+ {
+ struct berval *retdata;
+
+ RETVAL = ldap_whoami_s(ld, &retdata, sctrls, cctrls);
+
+ if (retdata != NULL)
+ authzid = ldap_strdup(retdata->bv_val);
+
+ ber_memfree(retdata);
+ }
+ OUTPUT:
+ RETVAL
+ authzid
int
ldap_result(ld, msgid, all, timeout, result)
LDAP * ld
int msgid
int all
- LDAP_CHAR * timeout
+ SV * timeout
LDAPMessage * result = NO_INIT
CODE:
{
- struct timeval *tv_timeout = NULL, timeoutbuf;
- if (atof(timeout) > 0 && timeout && *timeout)
- {
- tv_timeout = &timeoutbuf;
- tv_timeout->tv_sec = atof(timeout);
- tv_timeout->tv_usec = 0;
- }
- RETVAL = ldap_result(ld, msgid, all, NULL, &result);
+ struct timeval *tv_timeout = NULL;
+
+ tv_timeout = sv2timeval(timeout);
+
+ RETVAL = ldap_result(ld, msgid, all, tv_timeout, &result);
+ Safefree(tv_timeout);
}
OUTPUT:
RETVAL
@@ -951,6 +1138,33 @@ ldap_parse_result(ld, msg, errorcodep, matcheddnp, errmsgp, referrals_ref, serve
errmsgp
int
+ldap_parse_extended_result(ld, msg, retoidp, retdatap, freeit)
+ LDAP * ld
+ LDAPMessage * msg
+ char * retoidp = NO_INIT
+ char * retdatap = NO_INIT
+ int freeit
+ CODE:
+ {
+ struct berval *retdata;
+
+ retdata = ber_memalloc(sizeof(struct berval *));
+
+ RETVAL =
+ ldap_parse_extended_result(ld, msg, &retoidp,
+ &retdata, freeit);
+
+ if (retdata != NULL)
+ retdatap = ldap_strdup(retdata->bv_val);
+
+ ber_memfree(retdata);
+ }
+ OUTPUT:
+ RETVAL
+ retoidp
+ retdatap
+
+int
ldap_parse_intermediate(ld, msg, retoidp, retdatap, serverctrls_ref, freeit)
LDAP * ld
LDAPMessage * msg
@@ -1002,6 +1216,28 @@ ldap_parse_intermediate(ld, msg, retoidp, retdatap, serverctrls_ref, freeit)
retoidp
retdatap
+int
+ldap_parse_whoami(ld, msg, authzid)
+ LDAP * ld
+ LDAPMessage * msg
+ char * authzid = NO_INIT
+ CODE:
+ {
+ struct berval *retdata;
+
+ retdata = ber_memalloc(sizeof(struct berval *));
+
+ RETVAL =
+ ldap_parse_whoami(ld, msg, &retdata);
+
+ if (retdata != NULL)
+ authzid = ldap_strdup(retdata->bv_val);
+
+ ber_memfree(retdata);
+ }
+ OUTPUT:
+ RETVAL
+ authzid
char *
ldap_control_oid(control)
@@ -1014,12 +1250,12 @@ ldap_control_oid(control)
RETVAL
-char *
+SV *
ldap_control_berval(control)
LDAPControl * control
CODE:
{
- RETVAL = control->ldctl_value.bv_val;
+ RETVAL = newSVpv(control->ldctl_value.bv_val, control->ldctl_value.bv_len);
}
OUTPUT:
RETVAL
@@ -1436,18 +1672,16 @@ ldap_url_search_st(ld,url,attrsonly,timeout,result)
LDAP * ld
char * url
int attrsonly
- LDAP_CHAR * timeout
+ SV * timeout
LDAPMessage * result = NO_INIT
CODE:
{
- struct timeval *tv_timeout = NULL, timeoutbuf;
- if (timeout && *timeout)
- {
- tv_timeout = &timeoutbuf;
- tv_timeout->tv_sec = atof(timeout);
- tv_timeout->tv_usec = 0;
- }
+ struct timeval *tv_timeout = NULL;
+
+ tv_timeout = sv2timeval(timeout);
+
RETVAL = ldap_url_search_st(ld,url,attrsonly,tv_timeout,&result);
+ Safefree(tv_timeout);
}
OUTPUT:
RETVAL
@@ -1629,8 +1863,7 @@ ldap_create_control(oid, bv_val, bv_len, iscritical, ctrlp)
LDAPControl *ctrl = malloc(sizeof(LDAPControl));
ctrl->ldctl_oid = ber_strdup(oid);
- ctrl->ldctl_value.bv_val = ber_strdup(bv_val);
- ctrl->ldctl_value.bv_len = bv_len;
+ ber_mem2bv(bv_val, bv_len, 1, &ctrl->ldctl_value);
ctrl->ldctl_iscritical = iscritical;
ctrlp = ctrl;
diff --git a/MANIFEST b/MANIFEST
index 4addb2c..ed6093e 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -18,3 +18,30 @@ examples/updatepw.pl
examples/web500.pl
examples/www-ldap.pl
META.yml Module meta-data (added by MakeMaker)
+t/features/whoami.feature
+t/features/search.feature
+t/features/options.feature
+t/features/step_definitions/rename_steps.pl
+t/features/step_definitions/modify_steps.pl
+t/features/step_definitions/extended_operation_steps.pl
+t/features/step_definitions/server_controls_steps.pl
+t/features/step_definitions/options_steps.pl
+t/features/step_definitions/delete_steps.pl
+t/features/step_definitions/search_steps.pl
+t/features/step_definitions/add_steps.pl
+t/features/step_definitions/whoami_steps.pl
+t/features/step_definitions/general_steps.pl
+t/features/step_definitions/bind_steps.pl
+t/features/step_definitions/syncrepl_steps.pl
+t/features/step_definitions/compare_steps.pl
+t/features/compare.feature
+t/features/modify.feature
+t/features/syncrepl.feature
+t/features/rename.feature
+t/features/add.feature
+t/features/server_controls.feature
+t/features/extended_operations.feature
+t/features/bind.feature
+t/features/delete.feature
+t/01-bdd-cucumber.t
+t/test-config.pl
diff --git a/META.yml b/META.yml
deleted file mode 100644
index 6526d4b..0000000
--- a/META.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# http://module-build.sourceforge.net/META-spec.html
-#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX#
-name: Net-LDAPapi
-version: 3.0.3
-version_from: LDAPapi.pm
-installdirs: site
-requires:
- Convert::ASN1: 0.19
-
-distribution_type: module
-generated_by: ExtUtils::MakeMaker version 6.17
diff --git a/Makefile.PL b/Makefile.PL
index fa36724..fcb8f8c 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -128,4 +128,14 @@ WriteMakefile(
)),
'depend' => { 'LDAPapi.c' => 'constant.h' },
'clean' => { 'FILES' => 'constant.h' },
+ META_MERGE => {
+ 'meta-spec' => { version => 2 },
+ resources => {
+ repository => {
+ type => 'git',
+ url => 'https://github.com/quanah/net-ldapapi.git',
+ web => 'https://github.com/quanah/net-ldapapi',
+ },
+ },
+ },
);
diff --git a/README b/README
index f644df4..5756693 100644
--- a/README
+++ b/README
@@ -195,14 +195,17 @@ Perl-OO style interface if you have never used the C API.
FEEDBACK
========
- Any feedback should be directed to mishikal@yahoo.com
+ Any feedback should be directed to quanah.gibsonmount@gmail.com
+
BUGS
====
-The non-OO stuff should work well. Please let me know if I've introduced
+ The non-OO stuff should work well. Please let me know if I've introduced
any bugs in the OO stuff or the changed examples.
+ Please report bugs at github - https://github.com/quanah/net-ldapapi/issues/
+
--
Clayton Donley
Rolling Meadows, IL, USA
@@ -215,8 +218,8 @@ Chief Architect, Symas Corporation http://www.symas.com
Core Team, OpenLDAP Project http://www.openldap.org
Quanah Gibson-Mount
-email: mishikal@yahoo.com
+email: quanah.gibsonmount@gmail.com
CPAN: /by-authors/id/M/MI/MISHIKAL
-Principal Software Engineer
-Zimbra, Inc http://www.zimbra.com
+Platform Architect
+Synacor, Inc http://www.zimbra.com http://synacor.com/
Core Team, OpenLDAP Project http://www.openldap.org
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a7d9add
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+The Net::LDAPapi Perl Module uses the OpenLDAP and Mozilla C api's to directly access and manipulate an LDAP v2 or LDAP v3 server.
diff --git a/examples/ldapwalk-dieter.pl b/examples/ldapwalk-dieter.pl
new file mode 100755
index 0000000..e3321be
--- /dev/null
+++ b/examples/ldapwalk-dieter.pl
@@ -0,0 +1,176 @@
+#!/usr/bin/perl
+#
+# $Id: ldapwalk.pl,v 1.1 2007/07/01 20:14:40 dieter Exp dieter $
+# ldapwalk.pl - Walks through Records Matching a Given Filter
+# Author: Clayton Donley, Motorola, <donley@cig.mot.com>
+#
+# Demonstration of Synchronous Searching in PERL5.
+#
+# Rather than printing attribute and values directly, they are
+# stored in a Hash, where further manipulation would be very simple.
+# The output could then be printed to a file or standard output, or
+# simply run through the modify or add commands.
+#
+# Usage: ldapwalk.pl FILTER
+# Example: ldapwalk.pl "sn=Donley"
+#
+
+use strict;
+use Net::LDAPapi;
+
+# Define these values
+
+my $ldap_server = "localhost";
+my $BASEDN = "dc=example, dc=com";
+my $sizelimit = 100; # Set to Maximum Number of Entries to Return
+ # Can set small to test error routines
+my $deref = "search";
+
+# Various Variable Declarations...
+my $ld;
+my $dn;
+my $attr;
+my $ent;
+my $ber;
+my @vals;
+my %record;
+my $rc;
+my $result;
+
+#
+# Initialize Connection to LDAP Server
+
+if (($ld = new Net::LDAPapi($ldap_server)) == -1)
+{
+ die "Connection Failed!";
+}
+
+#ldap_set_option(0,LDAP_OPT_DEBUG_LEVEL,-1);
+
+#
+# Bind as NULL User to LDAP connection $ld
+
+$ld->sasl_parms(-mech=>"DIGEST-MD5",-flags=>LDAP_SASL_AUTOMATIC);
+
+if ($ld->bind_s("benchmark","xxx",LDAP_AUTH_SASL) != LDAP_SUCCESS)
+# if ($ld->bind_s != LDAP_SUCCESS)
+{
+ $ld->unbind;
+ die "bind: ", $ld->errstring, ": ", $ld->extramsg;
+}
+#
+# This will set the size limit to $sizelimit from above. The command
+# is a Netscape addition, but I've programmed replacement versions for
+# other APIs.
+$ld->set_option(LDAP_OPT_SIZELIMIT,$sizelimit);
+# $ld->set_option(LDAP_OPT_DEREF,$deref);
+
+# This routine is COMPLETELY unnecessary in this application, since
+# the rebind procedure at the end of this program simply rebinds as
+# a NULL user.
+#$ld->set_rebind_proc(&rebindproc);
+
+#
+# Specify Search Filter and List of Attributes to Return
+
+my $filter = $ARGV[0];
+my @attrs = ("cn","mail","telephonenumber");
+
+#
+# Perform Search
+my $msgid = $ld->search($BASEDN,LDAP_SCOPE_ONELEVEL,$filter,\@attrs,0);
+
+if ($msgid < 0)
+{
+ $ld->unbind;
+ die "search: ", $ld->errstring, ": ", $ld->extramsg;
+}
+
+# Reset Number of Entries Counter
+my $nentries = 0;
+
+# Set no timeout.
+my $timeout = -1;
+
+#
+# Cycle Through Entries
+while (($rc = $ld->result($msgid,0,$timeout)) == LDAP_RES_SEARCH_ENTRY)
+{
+ $nentries++;
+
+ for ($ent = $ld->first_entry; $ent != 0; $ent = $ld->next_entry)
+ {
+
+#
+# Get Full DN
+
+ if (($dn = $ld->get_dn) eq "")
+ {
+ $ld->unbind;
+ die "get_dn: ", $ld->errstring, ": ", $ld->extramsg;
+ }
+
+#
+# Cycle Through Each Attribute
+
+ for ($attr = $ld->first_attribute; $attr ne ""; $attr = $ld->next_attribute)
+ {
+
+#
+# Notice that we're using get_values_len. This will retrieve binary
+# as well as text data. You can change to get_values to only get text
+# data.
+#
+ @vals = $ld->get_values ($attr);
+ $record{$dn}->{$attr} = [@vals];
+ }
+ }
+ $ld->msgfree;
+
+}
+if ($rc == LDAP_RES_SEARCH_RESULT &&
+ $ld->err != LDAP_SUCCESS)
+{
+ $ld->unbind;
+ die "result: ", $ld->errstring, ": ", $ld->extramsg;
+}
+
+print "Found $nentries records\n";
+
+$ld->unbind;
+
+foreach $dn (keys %record)
+{
+ my $item;
+ print "dn: $dn\n";
+ foreach $attr (keys %{$record{$dn}})
+ {
+ for $item ( @{$record{$dn}{$attr}})
+ {
+ if ($attr =~ /binary/ )
+ {
+ print "$attr: <binary>\n";
+ } elsif ($attr eq "jpegphoto") {
+#
+# Notice how easy it is to take a binary attribute and dump it to a file
+# or such. Gotta love PERL.
+#
+ print "$attr: JpegPhoto (length: " . length($item). ")\n";
+ open (TEST,">$dn.jpg");
+ print TEST $item;
+ close (TEST);
+ } else {
+ print "$attr: $item\n";
+ }
+ }
+ }
+}
+
+exit;
+
+sub rebindproc
+{
+
+ return("","",LDAP_AUTH_SIMPLE);
+}
+
diff --git a/examples/ldapwalk.pl b/examples/ldapwalk.pl
index 3cfbbbe..91b1a86 100755
--- a/examples/ldapwalk.pl
+++ b/examples/ldapwalk.pl
@@ -40,7 +40,7 @@ my $result;
if (($ld = new Net::LDAPapi($ldap_server)) == -1)
{
- die "Connection Failed!";
+ die "Unable to initialize!";
}
#ldap_set_option(0,LDAP_OPT_DEBUG_LEVEL,-1);
@@ -53,8 +53,9 @@ if (($ld = new Net::LDAPapi($ldap_server)) == -1)
#if ($ld->bind_s("tester","tester",LDAP_AUTH_SASL) != LDAP_SUCCESS)
if ($ld->bind_s != LDAP_SUCCESS)
{
+ my $errstr=$ld->errstring;
$ld->unbind;
- die "bind: ", $ld->errstring, ": ", $ld->extramsg;
+ die "bind: ", $errstr;
}
# This will set the size limit to $sizelimit from above. The command
@@ -114,7 +115,7 @@ while (1)
#
# Cycle Through Each Attribute
- for ($attr = $ld->first_attribute; $attr ne ""; $attr = $ld->next_attribute)
+ for ($attr = $ld->first_attribute; defined($attr); $attr = $ld->next_attribute)
{
#
@@ -129,7 +130,7 @@ while (1)
$ld->msgfree;
}
-if ( $result == undef && $ld->err != LDAP_SUCCESS)
+if ( !defined($result) && $ld->err != LDAP_SUCCESS)
{
$ld->unbind;
die "result: ", $ld->errstring, ": ", $ld->extramsg;
@@ -164,6 +165,7 @@ foreach $dn (keys %record)
}
}
}
+ print "\n";
}
exit;
diff --git a/examples/ldapwalk2.pl b/examples/ldapwalk2.pl
index 759e31d..eb1665a 100755
--- a/examples/ldapwalk2.pl
+++ b/examples/ldapwalk2.pl
@@ -36,7 +36,7 @@ my $attr;
if (($ldcon = new Net::LDAPapi($ldap_server)) == -1)
{
- die "Unable to Open LDAP Connection";
+ die "Unable to initialize!";
}
if ($ldcon->bind_s != LDAP_SUCCESS)
diff --git a/examples/testurl.pl b/examples/testurl.pl
new file mode 100755
index 0000000..f6d9960
--- /dev/null
+++ b/examples/testurl.pl
@@ -0,0 +1,83 @@
+#!/usr/bin/perl -w
+#
+# testwrite.pl - Test of LDAP URL Operations in Perl5
+# Author: Clayton Donley <donley@cig.mot.com>
+#
+# This script tests some of the basic LDAP URL functions.
+# Call the script with an LDAP URL to perform a search.
+
+use strict;
+use Net::LDAPapi;
+
+my $urlhref;
+my $url = $ARGV[0] || "ldap://ldap.four11.com/??sub?(cn=Clayton Donley)";
+
+if (ldap_is_ldap_url($url))
+{
+ $urlhref = ldap_url_parse($url);
+} else {
+ die "$url: Not an LDAP Url.";
+}
+
+if ($urlhref)
+{
+ print "host: " . $urlhref->{'host'} . "\n";
+ print "port: " . $urlhref->{'port'} . "\n";
+ print "base: " . $urlhref->{'dn'} . "\n";
+
+ my $attr;
+ foreach $attr (@{$urlhref->{'attr'}})
+ {
+ print "attr: " . $attr . "\n";
+ }
+ print "filter: " . $urlhref->{'filter'} . "\n";
+ print "scope: " . $urlhref->{'scope'} . "\n";
+
+# If using Netscape, there is an options key specifying the use of SSL, etc...
+
+ if ($urlhref->{'options'})
+ {
+ print "options: " . $urlhref->{'options'} . "\n"
+ }
+
+ print "Connecting...\n";
+
+ my $port = $urlhref->{"port"} || 389;
+ my $ld = new Net::LDAPapi(-host=>$urlhref->{"host"},-port=>$port);
+
+ if ($ld == -1)
+ {
+ die "Connection failed...";
+ }
+
+ $ld->bind_s;
+
+ $ld->url_search_s($url,0);
+
+ my %record = %{$ld->get_all_entries};
+
+ $ld->unbind;
+
+ my @dns = (sort keys %record);
+ print $#dns+1 . " entries returned.\n";
+
+ foreach my $dn (@dns)
+ {
+ print "dn: $dn\n";
+ foreach my $attr (keys %{$record{$dn}})
+ {
+ foreach my $item (@{$record{$dn}{$attr}})
+ {
+ if ($attr =~ /binary/)
+ {
+ print "$attr: binary - length=" . length($item) . "\n";
+ } else {
+ print "$attr: $item\n";
+ }
+ }
+ }
+ }
+
+} else {
+ print "Invalid LDAP URL: $url\n";
+}
diff --git a/t/01-bdd-cucumber.t b/t/01-bdd-cucumber.t
new file mode 100644
index 0000000..87babd3
--- /dev/null
+++ b/t/01-bdd-cucumber.t
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+BEGIN {
+ require './t/test-config.pl';
+ if (!$RunDeveloperTests) {
+ print "1..0 # Skipped: Developer tests are not enabled";
+
+ exit;
+ }
+};
+
+
+use strict;
+use warnings;
+use Devel::Cover;
+use Test::More;
+
+# This will find step definitions and feature files in the directory you point
+# it at below
+use Test::BDD::Cucumber::Loader;
+
+# This harness prints out nice TAP
+use Test::BDD::Cucumber::Harness::TestBuilder;
+
+# Load a directory with Cucumber files in it. It will recursively execute any
+# file matching .*_steps.pl as a Step file, and .*\.feature as a feature file.
+# The features are returned in @features, and the executor is created with the
+# step definitions loaded.
+my ( $executor, @features ) = Test::BDD::Cucumber::Loader->load(
+ 't/features/' );
+
+# Create a Harness to execute against. TestBuilder harness prints TAP
+my $harness = Test::BDD::Cucumber::Harness::TestBuilder->new({});
+
+# For each feature found, execute it, using the Harness to print results
+$executor->execute( $_, $harness ) for @features;
+done_testing();
diff --git a/t/features/add.feature b/t/features/add.feature
new file mode 100644
index 0000000..c7a0d61
--- /dev/null
+++ b/t/features/add.feature
@@ -0,0 +1,24 @@
+Feature: Adding entries to the directory
+ As a directory consumer
+ I want to ensure that I can add entries to the directory
+ In order to store information
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can add a new entry to the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ Then the new entry result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously add a new entry to the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And the test container has been deleted
diff --git a/t/features/bind.feature b/t/features/bind.feature
new file mode 100644
index 0000000..74b19be
--- /dev/null
+++ b/t/features/bind.feature
@@ -0,0 +1,40 @@
+Feature: Binding to the directory
+ As a directory consumer
+ I want to ensure that I can bind properly to directories
+ In order to establish my identity
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can bind anonymously
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with anonymous authentication to the directory
+ Then the bind result is LDAP_SUCCESS
+
+ Scenario: Can bind with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with simple authentication to the directory
+ Then the bind result is LDAP_SUCCESS
+
+ Scenario: Can bind with sasl authentication
+ Given a Net::LDAPapi object that has been connected to the ldapi LDAP server
+ When I've bound with sasl authentication to the directory
+ Then the bind result is LDAP_SUCCESS
+
+ Scenario: Can asynchronously bind anonymously
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with anonymous authentication to the directory
+ Then the bind result message type is LDAP_RES_BIND
+ And the bind result is LDAP_SUCCESS
+
+ Scenario: Can asynchronously bind with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with simple authentication to the directory
+ Then the bind result message type is LDAP_RES_BIND
+ And the bind result is LDAP_SUCCESS
+
+# Scenario: Can asynchronously bind with sasl authentication
+# Given a Net::LDAPapi object that has been connected to the ldapi LDAP server
+# When I've asynchronously bound with sasl authentication to the directory
+# Then the bind result message type is LDAP_RES_BIND
+# And the bind result is LDAP_SUCCESS
diff --git a/t/features/compare.feature b/t/features/compare.feature
new file mode 100644
index 0000000..f6f8406
--- /dev/null
+++ b/t/features/compare.feature
@@ -0,0 +1,29 @@
+Feature: Comparing values to values of attributes of entries within the directory
+ As a directory consumer
+ I want to ensure that I can test the value of an attribute on an entry within the directory
+ In order to perform simple comparisons
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can compare an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've compared to an attribute on the new entry
+ Then the new entry result is LDAP_SUCCESS
+ And the new entry comparison result is LDAP_COMPARE_TRUE
+ And the test container has been deleted
+
+ Scenario: Can asynchronously compare an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously compared to an attribute on the new entry
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the new entry comparison result message type is LDAP_RES_COMPARE
+ And the new entry comparison result is LDAP_COMPARE_TRUE
+ And the test container has been deleted
diff --git a/t/features/delete.feature b/t/features/delete.feature
new file mode 100644
index 0000000..663e08b
--- /dev/null
+++ b/t/features/delete.feature
@@ -0,0 +1,29 @@
+Feature: Deleting entries from the directory
+ As a directory consumer
+ I want to ensure that I can delete entries from the directory
+ In order to remove information
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can remove an entry from the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've deleted the new entry from the directory
+ Then the new entry result is LDAP_SUCCESS
+ And the delete entry result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously remove an entry from the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously deleted the new entry from the directory
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the delete entry result message type is LDAP_RES_DELETE
+ And the delete entry result is LDAP_SUCCESS
+ And the test container has been deleted
diff --git a/t/features/extended_operations.feature b/t/features/extended_operations.feature
new file mode 100644
index 0000000..ba922f5
--- /dev/null
+++ b/t/features/extended_operations.feature
@@ -0,0 +1,51 @@
+Feature: Executing extended operations against the directory
+ As a directory consumer
+ I want to ensure that I can execute extended operations against the directory
+ In order to use arbitrary LDAPv3 extensions
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can match identities retrieved with native whoami and using extended operations with anonymous authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with anonymous authentication to the directory
+ And I've queried the directory for my identity
+ And I've issued a whoami extended operation to the directory
+ Then the identity result is LDAP_SUCCESS
+ And the whoami extended operation result is LDAP_SUCCESS
+ And the identity matches
+ And the whoami extended operation matches
+
+ Scenario: Can match identities retrieved with native whoami and using extended operations with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with simple authentication to the directory
+ And I've queried the directory for my identity
+ And I've issued a whoami extended operation to the directory
+ Then the identity result is LDAP_SUCCESS
+ And the whoami extended operation result is LDAP_SUCCESS
+ And the identity matches
+ And the whoami extended operation matches
+
+ Scenario: Can asynchronously match identities retrieved with native whoami and using extended operations with anonymous authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with anonymous authentication to the directory
+ And I've asynchronously queried the directory for my identity
+ And I've asynchronously issued a whoami extended operation to the directory
+ Then after waiting for all results, the identity result message type is LDAP_RES_EXTENDED
+ And the identity result is LDAP_SUCCESS
+ And after waiting for all results, the whoami extended operation result message type is LDAP_RES_EXTENDED
+ And the whoami extended operation result is LDAP_SUCCESS
+ And the identity matches
+ And the whoami extended operation matches
+
+ Scenario: Can asynchronously match identities retrieved with native whoami and using extended operations with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with simple authentication to the directory
+ And I've asynchronously queried the directory for my identity
+ And I've asynchronously issued a whoami extended operation to the directory
+ Then after waiting for all results, the identity result message type is LDAP_RES_EXTENDED
+ And the identity result is LDAP_SUCCESS
+ And after waiting for all results, the whoami extended operation result message type is LDAP_RES_EXTENDED
+ And the whoami extended operation result is LDAP_SUCCESS
+ And the identity matches
+ And the whoami extended operation matches
diff --git a/t/features/modify.feature b/t/features/modify.feature
new file mode 100644
index 0000000..38fab2a
--- /dev/null
+++ b/t/features/modify.feature
@@ -0,0 +1,83 @@
+Feature: Updating attributes of entries within the directory
+ As a directory consumer
+ I want to ensure that I can adjust attributes on entries within the directory
+ In order to extend or update entries with new or updated information
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can add a new attribute to an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've added a new attribute to the new entry
+ Then the new entry result is LDAP_SUCCESS
+ And the new attribute result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can modify an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've added a new attribute to the new entry
+ And I've modified the new attribute on the new entry
+ Then the new entry result is LDAP_SUCCESS
+ And the new attribute result is LDAP_SUCCESS
+ And the modified attribute result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can remove an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've added a new attribute to the new entry
+ And I've removed the new attribute from the new entry
+ Then the new entry result is LDAP_SUCCESS
+ And the new attribute result is LDAP_SUCCESS
+ And the removed attribute result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously add a new attribute to an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously added a new attribute to the new entry
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the new attribute result message type is LDAP_RES_MODIFY
+ And the new attribute result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously modify an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously added a new attribute to the new entry
+ And I've asynchronously modified the new attribute on the new entry
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the new attribute result message type is LDAP_RES_MODIFY
+ And the new attribute result is LDAP_SUCCESS
+ And after waiting for all results, the modified attribute result message type is LDAP_RES_MODIFY
+ And the modified attribute result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously remove an attribute on an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously added a new attribute to the new entry
+ And I've asynchronously removed the new attribute from the new entry
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the new attribute result message type is LDAP_RES_MODIFY
+ And the new attribute result is LDAP_SUCCESS
+ And after waiting for all results, the removed attribute result message type is LDAP_RES_MODIFY
+ And the removed attribute result is LDAP_SUCCESS
+ And the test container has been deleted
diff --git a/t/features/options.feature b/t/features/options.feature
new file mode 100644
index 0000000..e432f04
--- /dev/null
+++ b/t/features/options.feature
@@ -0,0 +1,12 @@
+Feature: Control of options used to configure the LDAP library
+ As a directory consumer
+ I want to ensure that I can control the options that are used to configure the LDAP client library
+ In order to alter behaviour according to my needs
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can set and read back options
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've set option LDAP_OPT_SIZELIMIT with value 200
+ Then option LDAP_OPT_SIZELIMIT has value 200
diff --git a/t/features/rename.feature b/t/features/rename.feature
new file mode 100644
index 0000000..653dcd0
--- /dev/null
+++ b/t/features/rename.feature
@@ -0,0 +1,34 @@
+Feature: Renaming entries within the directory
+ As a directory consumer
+ I want to ensure that I can rename entries within the directory
+ In order to reorganise information
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can rename an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've added a new entry to the directory
+ And I've added a new container to the directory
+ And I've moved the new entry to the new container
+ Then the new entry result is LDAP_SUCCESS
+ And the new container result is LDAP_SUCCESS
+ And the rename entry result is LDAP_SUCCESS
+ And the test container has been deleted
+
+ Scenario: Can asynchronously rename an entry within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And a test container has been created
+ And I've asynchronously added a new entry to the directory
+ And I've asynchronously added a new container to the directory
+ And I've asynchronously moved the new entry to the new container
+ Then after waiting for all results, the new entry result message type is LDAP_RES_ADD
+ And the new entry result is LDAP_SUCCESS
+ And after waiting for all results, the new container result message type is LDAP_RES_ADD
+ And the new container result is LDAP_SUCCESS
+ And after waiting for all results, the rename entry result message type is LDAP_RES_MODDN
+ And the rename entry result is LDAP_SUCCESS
+ And the test container has been deleted
diff --git a/t/features/search.feature b/t/features/search.feature
new file mode 100644
index 0000000..88f43b8
--- /dev/null
+++ b/t/features/search.feature
@@ -0,0 +1,77 @@
+Feature: Searching the directory
+ As a directory consumer
+ I want to ensure that I can search the directory
+ In order to find relevant entries
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can find objects that exist within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+
+ Scenario: Can asynchronously find objects that exist within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And I've asynchronously searched for records with scope LDAP_SCOPE_SUBTREE
+ Then after waiting for all results, the search result message type is LDAP_RES_SEARCH_RESULT
+ And the search result is LDAP_SUCCESS
+ And the search count matches
+
+ Scenario: Can find objects that exist within the directory with a timeout
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE, with timeout 1
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+
+ Scenario: Can asynchronously find objects that exist within the directory with a timeout
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with default authentication to the directory
+ And I've asynchronously searched for records with scope LDAP_SCOPE_SUBTREE, with timeout 1
+ Then after waiting for all results, the search result message type is LDAP_RES_SEARCH_RESULT
+ And the search result is LDAP_SUCCESS
+ And the search count matches
+
+ Scenario: Can find objects that exist with the directory and iterate them with next_entry and next_attribute
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using next_entry for each entry returned the dn and all attributes using next_attribute are valid
+
+ Scenario: Can find objects that exist with the directory and iterate them with next_entry and entry_attribute
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using next_entry for each entry returned the dn and all attributes using entry_attribute are valid
+
+ Scenario: Can find objects that exist with the directory and iterate them with result_entry and next_attribute
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using result_entry for each entry returned the dn and all attributes using next_attribute are valid
+
+ Scenario: Can find objects that exist with the directory and iterate them with result_entry and entry_attribute
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using result_entry for each entry returned the dn and all attributes using entry_attribute are valid
+
+ Scenario: Can find objects that exist with the directory and read them all at once
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using get_all_entries for each entry returned the dn and all attributes are valid
diff --git a/t/features/server_controls.feature b/t/features/server_controls.feature
new file mode 100644
index 0000000..3b4cee4
--- /dev/null
+++ b/t/features/server_controls.feature
@@ -0,0 +1,21 @@
+Feature: Using server controls to control results
+ As a directory consumer
+ I want to ensure that I can use server controls when querying the directory
+ In order to be able to utilise the extended features of my directory
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can use the Server Side Sort control and Virtual List View Control
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ And the server side sort control definition
+ And the virtual list view control definition
+ When I've bound with default authentication to the directory
+ And I've created a server side sort control
+ And I've created a virtual list view control
+ And I've searched for records with scope LDAP_SCOPE_SUBTREE, with server controls server side sort and virtual list view
+ Then the search result is LDAP_SUCCESS
+ And the search count matches
+ And using next_entry for each entry returned the dn and all attributes using next_attribute are valid
+ And the server side sort control was successfully used
+ And the virtual list view control was successfully used
diff --git a/t/features/step_definitions/add_steps.pl b/t/features/step_definitions/add_steps.pl
new file mode 100644
index 0000000..5615699
--- /dev/null
+++ b/t/features/step_definitions/add_steps.pl
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've (asynchronously )?added a new (entry|container) to the directory/i, sub {
+ my $async = $1 ? 1 : 0;
+ my $type = lc($2);
+
+ S->{'new ' . $type . '_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'add_ext_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'add_ext';
+ }
+ S->{'new ' . $type . '_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{$type . '_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-mod'} = $TestConfig{'data'}{$type . '_attributes'};
+
+ S->{'new ' . $type . '_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/bind_steps.pl b/t/features/step_definitions/bind_steps.pl
new file mode 100644
index 0000000..ed9fdaa
--- /dev/null
+++ b/t/features/step_definitions/bind_steps.pl
@@ -0,0 +1,53 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+#use Net::LDAPapi;
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+When qr/I've (asynchronously )?bound with (.+?) authentication to the directory/, sub {
+ my $async = $1 ? 1 : 0;
+ my $type = lc($2);
+
+ my $func = "bind_s";
+ my %args = ();
+
+ if ($async) {
+ $func = "bind";
+ }
+ S->{'bind_async'} = $async;
+
+ if ($type eq "default") {
+ $type = lc($TestConfig{'ldap'}{'default_bind_type'});
+ }
+ S->{'bind_type'} = $type;
+
+ S->{'bind_result'} = "skipped";
+
+ if ($type eq "anonymous") {
+ return if $TestConfig{'ldap'}{'bind_types'}{'anonymous'}{'enabled'} != 1;
+ } elsif ($type eq "simple") {
+ return if $TestConfig{'ldap'}{'bind_types'}{'simple'}{'enabled'} != 1;
+
+ %args = (
+ -dn => $TestConfig{'ldap'}{'bind_types'}{'simple'}{'bind_dn'},
+ -password => $TestConfig{'ldap'}{'bind_types'}{'simple'}{'bind_pw'}
+ );
+ } elsif ($type eq "sasl") {
+ return if $TestConfig{'ldap'}{'bind_types'}{'sasl'}{'enabled'} != 1;
+
+ S->{'object'}->sasl_parms(%{$TestConfig{'ldap'}{'bind_types'}{'sasl'}{'sasl_parms'}});
+
+ %args = (
+ -type => S->{'object'}->LDAP_AUTH_SASL
+ );
+ }
+
+ S->{'bind_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/compare_steps.pl b/t/features/step_definitions/compare_steps.pl
new file mode 100644
index 0000000..2b4a8c2
--- /dev/null
+++ b/t/features/step_definitions/compare_steps.pl
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've (asynchronously )?compared to an attribute on the new (entry|container)/i, sub {
+ my $async = $1 ? 1 : 0;
+ my $type = lc($2);
+
+ S->{'new ' . $type . ' comparison_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'compare_ext_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'compare_ext';
+ }
+ S->{'new ' . $type . ' comparison_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{$type . '_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-attr'} = $TestConfig{'compare'}{$type . '_attribute'};
+ $args{'-value'} = $TestConfig{'data'}{$type . '_attributes'}{$args{'-attr'}};
+
+ S->{'new ' . $type . ' comparison_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/delete_steps.pl b/t/features/step_definitions/delete_steps.pl
new file mode 100644
index 0000000..41cddc6
--- /dev/null
+++ b/t/features/step_definitions/delete_steps.pl
@@ -0,0 +1,34 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've (asynchronously )?deleted the new (entry|container) from the directory/i, sub {
+ my $async = $1 ? 1 : 0;
+ my $type = lc($2);
+
+ S->{'delete ' . $type . '_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'delete_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'delete';
+ }
+ S->{'delete ' . $type . '_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{$type . '_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+
+ S->{'delete ' . $type . '_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/extended_operation_steps.pl b/t/features/step_definitions/extended_operation_steps.pl
new file mode 100644
index 0000000..d8c7c3a
--- /dev/null
+++ b/t/features/step_definitions/extended_operation_steps.pl
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+
+When qr/I've (asynchronously )?issued a (.+?) extended operation to the directory/i, sub {
+ my $async = $1 ? 1 : 0;
+ my $extended_operation = lc($2);
+
+ S->{$extended_operation . ' extended operation_result'} = "skipped";
+
+ return if S->{"bind_result"} eq "skipped";
+
+ my $func = "extended_operation_s";
+ my %args = ();
+
+ if ($async) {
+ $func = "extended_operation";
+ }
+ S->{$extended_operation . ' extended operation_async'} = $async;
+
+ if ($extended_operation eq "whoami") {
+ $args{'-oid'} = '1.3.6.1.4.1.4203.1.11.3';
+ $args{'-result'} = \%{S->{'whoami extended operation_authzid'}};
+ }
+
+ S->{$extended_operation . ' extended operation_result'} = S->{'object'}->$func(%args);
+};
+
+Then qr/the (.+?) extended operation matches/i, sub {
+ my $extended_operation = lc($1);
+
+ my $async = S->{$extended_operation . ' extended operation_async'};
+
+ my $got = undef;
+ if ($async) {
+ $got = {S->{'object'}->parse_extended_result(S->{$extended_operation . ' extended operation_result_id'})};
+ }
+
+ if ($extended_operation eq "whoami") {
+ if (!$async) {
+ $got = S->{$extended_operation . ' extended operation_authzid'};
+ }
+
+ is($got->{'retdatap'}, S->{'identity_got'}, 'Does ' . ($async ? 'asynchronous ' : '' ) . ' whoami extended_operation match native whoami?');
+ } else {
+ TODO: {
+ todo_skip "$extended_operation matching unimplemented", 1;
+ }
+ }
+};
+
+1;
diff --git a/t/features/step_definitions/general_steps.pl b/t/features/step_definitions/general_steps.pl
new file mode 100644
index 0000000..40dbe5e
--- /dev/null
+++ b/t/features/step_definitions/general_steps.pl
@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Net::LDAPapi;
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+Given qr/a usable (\S+) class/, sub { use_ok($1); };
+Given qr/a Net::LDAPapi object that has been connected to the (.+?)?\s?LDAP server/, sub {
+ my $type = $1;
+
+ if (!defined($type)) {
+ $type = $TestConfig{'ldap'}{'default_server'};
+ }
+
+ my $object = Net::LDAPapi->new(%{$TestConfig{'ldap'}{'server'}{$type}});
+
+ ok( $object, 'Net::LDAPapi object created');
+
+ S->{'object'} = $object;
+};
+
+When qr/a test container has been created/, sub {
+ my %args = ();
+
+ $args{'-dn'} = sprintf('%s,%s', $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-mod'} = $TestConfig{'data'}{'test_container_attributes'};
+
+ my $status = S->{'object'}->add_s(%args);
+
+ is(ldap_err2string($status), ldap_err2string(LDAP_SUCCESS), 'Was adding the test container successful?');
+};
+
+Then qr/the test container has been deleted/, sub {
+ my %search_args = ();
+ my @delete_dns = ();
+
+ $search_args{'-basedn'} = sprintf('%s,%s', $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $search_args{'-scope'} = LDAP_SCOPE_SUBTREE;
+ $search_args{'-filter'} = '(objectClass=*)';
+ $search_args{'-attrs'} = ['objectClass'];
+
+ my $search_status = S->{'object'}->search_s(%search_args);
+
+ is(ldap_err2string($search_status), ldap_err2string(LDAP_SUCCESS), 'Was searching for the test container to delete successful?');
+
+ while( my $ent = S->{'object'}->result_entry) {
+ push(@delete_dns, S->{'object'}->get_dn());
+ }
+
+ foreach my $dn (sort { length($b) <=> length($a) } @delete_dns) {
+ my %delete_args = ('-dn' => $dn);
+
+ my $status = S->{'object'}->delete_s(%delete_args);
+ is(ldap_err2string($status), ldap_err2string(LDAP_SUCCESS), 'Was deleting test container contents "' . $dn . '" successful?');
+ }
+};
+
+Then qr/(after waiting for all results, )?the (.+) result message type is (.+)/, sub {
+ my $wait_for_all = $1 ? 1 : 0;
+ my $test_function = $2;
+ my $desired_result = $3;
+
+ SKIP: {
+
+ skip(C->{'scenario'}->{'name'} . " skipped", 1) if S->{$test_function . '_result'} eq "skipped";
+
+ isnt( S->{$test_function . '_result'}, undef, "Do we have result from $test_function?");
+
+ if (is( S->{$test_function . '_async'}, 1, "Was $test_function asynchronous?")) {
+ S->{$test_function . '_result_id'} = S->{'object'}->result(S->{$test_function . '_result'}, $wait_for_all, 1);
+
+ is(S->{'object'}->msgtype2str(S->{'object'}->{"status"}), $desired_result, "Does expected result message type match?");
+ }
+
+ }
+};
+
+Then qr/the (.+) result is (.+)/, sub {
+ my $test_function = $1;
+ my $desired_result = $2;
+
+ SKIP: {
+
+ skip(C->{'scenario'}->{'name'} . " skipped", 1) if S->{$test_function . '_result'} eq "skipped";
+
+ if (isnt( S->{$test_function . '_result'}, undef, "Do we have result from $test_function?")) {
+
+ if (S->{$test_function . '_async'}) {
+ my $ref = {S->{'object'}->parse_result(S->{$test_function . '_result_id'})};
+
+ is(ldap_err2string($ref->{'errcode'}), ldap_err2string(S->{'object'}->$desired_result), "Does expected async result code match?");
+ } else {
+ is(ldap_err2string(S->{$test_function . '_result'}), ldap_err2string(S->{'object'}->$desired_result), "Does expected result code match?");
+ }
+ }
+ }
+};
+
+1;
diff --git a/t/features/step_definitions/modify_steps.pl b/t/features/step_definitions/modify_steps.pl
new file mode 100644
index 0000000..6e6e92d
--- /dev/null
+++ b/t/features/step_definitions/modify_steps.pl
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've (asynchronously )?added a new attribute to the new entry/i, sub {
+ my $async = $1 ? 1 : 0;
+
+ S->{'new attribute_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'modify_ext_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'modify_ext';
+ }
+ S->{'new attribute_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{'entry_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-mod'} = $TestConfig{'modify'}{'new_attribute'};
+
+ S->{'new attribute_result'} = S->{'object'}->$func(%args);
+};
+
+When qr/I've (asynchronously )?modified the new attribute on the new entry/i, sub {
+ my $async = $1 ? 1 : 0;
+
+ S->{'modified attribute_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'modify_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'modify';
+ }
+ S->{'modified attribute_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{'entry_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-mod'} = $TestConfig{'modify'}{'modify_attribute'};
+
+ S->{'modified attribute_result'} = S->{'object'}->$func(%args);
+};
+
+When qr/I've (asynchronously )?removed the new attribute from the new entry/i, sub {
+ my $async = $1 ? 1 : 0;
+
+ S->{'removed attribute_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'modify_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'modify';
+ }
+ S->{'removed attribute_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{'entry_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-mod'} = $TestConfig{'modify'}{'remove_attribute'};
+
+ S->{'removed attribute_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/options_steps.pl b/t/features/step_definitions/options_steps.pl
new file mode 100644
index 0000000..43e0b6a
--- /dev/null
+++ b/t/features/step_definitions/options_steps.pl
@@ -0,0 +1,33 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Net::LDAPapi;
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+When qr/I've set option (.+?) with value (.+)/i, sub {
+ my $option = $1;
+ my $value = int($2);
+
+ my $status = S->{'object'}->set_option(S->{'object'}->$option, $value);
+
+ is(ldap_err2string($status), ldap_err2string(LDAP_SUCCESS), "Was option successfully set?");
+};
+
+Then qr/option (.+?) has value (.+)/i, sub {
+ my $option = $1;
+ my $value = $2;
+
+ my $data;
+
+ my $status = S->{'object'}->get_option(S->{'object'}->$option, \$data);
+
+ is(ldap_err2string($status), ldap_err2string(LDAP_SUCCESS), "Was option successfully retrieved?");
+ is($data, $value, "Is the option set to the expected value?");
+};
+
+1;
diff --git a/t/features/step_definitions/rename_steps.pl b/t/features/step_definitions/rename_steps.pl
new file mode 100644
index 0000000..3005c52
--- /dev/null
+++ b/t/features/step_definitions/rename_steps.pl
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've (asynchronously )?moved the new entry to the new container/i, sub {
+ my $async = $1 ? 1 : 0;
+
+ S->{'rename entry_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'rename_s';
+ my %args = ();
+
+ if ($async) {
+ $func = 'rename';
+ }
+ S->{'rename entry_async'} = $async;
+
+ $args{'-dn'} = sprintf('%s,%s,%s', $TestConfig{'data'}{'entry_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-newsuper'} = sprintf('%s,%s,%s', $TestConfig{'rename'}{'new_super'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-newrdn'} = $TestConfig{'rename'}{'new_rdn'};
+
+ S->{'rename entry_result'} = S->{'object'}->$func(%args);
+};
+
+1;
diff --git a/t/features/step_definitions/search_steps.pl b/t/features/step_definitions/search_steps.pl
new file mode 100644
index 0000000..43287a7
--- /dev/null
+++ b/t/features/step_definitions/search_steps.pl
@@ -0,0 +1,105 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+use Data::Dumper;
+
+When qr/I've (asynchronously )?searched for records with scope ([^, ]+)(?:, with server control(?:s)? (.+((?:(,|and) .+)*)))?(?:, with timeout (\d+))?/, sub {
+ my $async = $1 ? 1 : 0;
+ my $scope = $2;
+ my $timeout = $6;
+
+ my @server_ctrls = $3 ? map { S->{'server_controls'}{$_} } split(/\s*(?:,|and)\s*/, $3) : undef;
+
+ my $func = "search_ext_s";
+ if ($async) {
+ $func = "search_ext";
+ }
+
+ S->{'search_async'} = $async;
+
+ my %args = ();
+
+ S->{'search_result'} = S->{'object'}->$func(
+ -basedn => $TestConfig{'ldap'}{'base_dn'},
+ -scope => S->{'object'}->$scope,
+ -filter => $TestConfig{'search'}{'filter'},
+ -attrs => \@{['cn']},
+ -attrsonly => 0,
+ -sctrls => [@server_ctrls],
+ -timeout => $timeout);
+};
+
+Then qr/the search count matches/, sub {
+ is(S->{'object'}->count_entries, $TestConfig{'search'}{'count'}, "Does the search count match?");
+};
+
+Then qr/using get_all_entries for each entry returned the dn and all attributes are valid/, sub {
+ my $entries = S->{'object'}->get_all_entries();
+
+ foreach my $entry_dn (keys %{$entries}) {
+ isnt($entry_dn, "", "Is the dn for the entry empty?");
+
+ foreach my $attribute (keys %{$entries->{$entry_dn}}) {
+ my @vals = $entries->{$entry_dn}{$attribute};
+
+ ok(($#vals >= 0), "Are values returned?");
+ }
+ }
+};
+
+Then qr/using (.+) for each entry returned the dn and all attributes using (.+?) are valid/, sub {
+ my $entry_iterate_mode = lc($1);
+ my $attribute_iterate_mode = lc($2);
+
+ my $attribute_tests = sub {
+ my $attr = shift;
+ my @vals = S->{'object'}->get_values($attr);
+
+ ok(($#vals >= 0), "Are values returned?");
+
+ };
+
+ my $attribute_block = sub {
+ if ($attribute_iterate_mode eq "next_attribute") {
+ for (my $attr = S->{'object'}->first_attribute; $attr; $attr = S->{'object'}->next_attribute) {
+ $attribute_tests->($attr);
+ }
+ } elsif ($attribute_iterate_mode eq "entry_attribute") {
+ foreach my $attr (S->{'object'}->entry_attribute) {
+ $attribute_tests->($attr);
+ }
+ }
+ };
+
+ my $entry_tests = sub {
+ isnt(S->{'object'}->get_dn(), "", "Is the dn for the entry empty?");
+ };
+
+ if ($entry_iterate_mode eq "next_entry") {
+ my $ent = S->{'object'}->first_entry;
+
+ my %ent_result = S->{'object'}->parse_result();
+ S->{'cache'}{'serverctrls'} = $ent_result{'serverctrls'};
+
+ for (; $ent; $ent = S->{'object'}->next_entry) {
+ $entry_tests->($ent);
+
+ $attribute_block->($ent);
+ }
+ } elsif ($entry_iterate_mode eq "result_entry") {
+ foreach my $ent (S->{'object'}->result_entry) {
+ $entry_tests->($ent);
+
+ $attribute_block->($ent);
+ }
+ }
+
+};
+
+1;
diff --git a/t/features/step_definitions/server_controls_steps.pl b/t/features/step_definitions/server_controls_steps.pl
new file mode 100644
index 0000000..4f27e3f
--- /dev/null
+++ b/t/features/step_definitions/server_controls_steps.pl
@@ -0,0 +1,136 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+use Net::LDAPapi;
+use Convert::ASN1;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+Given qr/the server side sort control definition/i, sub {
+ if (!defined(S->{'asn'}{'server side sort'})) {
+ S->{'asn'}{'server side sort'} = Convert::ASN1->new;
+
+ S->{'asn'}{'server side sort'}->prepare(<<ASN) or die "prepare: ", S->{'asn'}{'server side sort'}->error;
+
+ SortKey ::= SEQUENCE {
+ attributeType OCTET STRING,
+ orderingRule [0] OCTET STRING OPTIONAL,
+ reverseOrder [1] BOOLEAN }
+
+ SortKeyList ::= SEQUENCE OF SortKey
+
+ SortResult ::= SEQUENCE {
+ sortResult ENUMERATED,
+ attributeType [0] OCTET STRING OPTIONAL }
+
+ASN
+ }
+};
+
+Given qr/the virtual list view control definition/i, sub {
+ if (!defined(S->{'asn'}{'virtual list view'})) {
+ S->{'asn'}{'virtual list view'} = Convert::ASN1->new;
+
+ S->{'asn'}{'virtual list view'}->prepare(<<ASN) or die "prepare: ", S->{'asn'}{'virtual list view'}->error;
+
+ VirtualListViewRequest ::= SEQUENCE {
+ beforeCount INTEGER,
+ afterCount INTEGER,
+ target CHOICE {
+ byOffset [0] SEQUENCE {
+ offset INTEGER,
+ contentCount INTEGER
+ },
+ greaterThanOrEqual [1] OCTET STRING
+ },
+ contextID OCTET STRING OPTIONAL
+ }
+
+ VirtualListViewResponse ::= SEQUENCE {
+ targetPosition INTEGER,
+ contentCount INTEGER,
+ virtualListViewResult ENUMERATED,
+ contextID OCTET STRING OPTIONAL
+ }
+
+ASN
+ }
+};
+
+When qr/I've created a server side sort control/i, sub {
+ my $sss = S->{'asn'}{'server side sort'}->find('SortKeyList');
+
+ my $sss_berval = $sss->encode($TestConfig{'server_controls'}{'sss'}) or die S->{'asn'}{'server side sort'}->error;
+
+ my $sss_ctrl = S->{'object'}->create_control(
+ -oid => '1.2.840.113556.1.4.473',
+ -berval => $sss_berval,
+ );
+
+ S->{'server_controls'}{'server side sort'} = $sss_ctrl;
+};
+
+When qr/I've created a virtual list view control/i, sub {
+ my $vlv = S->{'asn'}{'virtual list view'}->find('VirtualListViewRequest');
+
+ my $vlv_berval = $vlv->encode($TestConfig{'server_controls'}{'vlv'}) or die S->{'asn'}{'virtual list view'}->error;
+
+ my $vlv_ctrl = S->{'object'}->create_control(
+ -oid => '2.16.840.1.113730.3.4.9',
+ -berval => $vlv_berval,
+ );
+
+ S->{'server_controls'}{'virtual list view'} = $vlv_ctrl;
+};
+
+Then qr/the server side sort control was successfully used/i, sub {
+ my $sss_response = S->{'asn'}{'server side sort'}->find('SortResult');
+
+ my $berval = undef;
+
+ foreach my $ctrl (@{S->{'cache'}{'serverctrls'}}) {
+ my $ctrl_oid = S->{'object'}->get_control_oid($ctrl);
+
+ if ($ctrl_oid eq '1.2.840.113556.1.4.474') {
+ $berval = S->{'object'}->get_control_berval($ctrl);
+ last;
+ }
+ }
+
+ isnt($berval, undef, "Was a berval returned?");
+
+ my $result = $sss_response->decode($berval) || ok(0, $sss_response->error);
+
+ is(ldap_err2string($result->{'sortResult'}), ldap_err2string(LDAP_SUCCESS), "Does server side sort result code match?");
+};
+
+Then qr/the virtual list view control was successfully used/i, sub {
+ my $vlv_response = S->{'asn'}{'virtual list view'}->find('VirtualListViewResponse');
+
+ my $berval = undef;
+
+ foreach my $ctrl (@{S->{'cache'}{'serverctrls'}}) {
+ my $ctrl_oid = S->{'object'}->get_control_oid($ctrl);
+
+ if ($ctrl_oid eq '2.16.840.1.113730.3.4.10') {
+ $berval = S->{'object'}->get_control_berval($ctrl);
+ last;
+ }
+ }
+
+ isnt($berval, undef, "Was a berval returned?");
+
+ my $result = $vlv_response->decode($berval) || ok(0, $vlv_response->error);
+
+ is(ldap_err2string($result->{'virtualListViewResult'}), ldap_err2string(LDAP_SUCCESS), "Does virtual list view result code match?");
+};
+
+
+1;
diff --git a/t/features/step_definitions/syncrepl_steps.pl b/t/features/step_definitions/syncrepl_steps.pl
new file mode 100644
index 0000000..0725c32
--- /dev/null
+++ b/t/features/step_definitions/syncrepl_steps.pl
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Net::LDAPapi;
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+use Data::Dumper;
+
+When qr/I've started listening for changes within the directory/i, sub {
+
+ S->{'listen changes_result'} = 'skipped';
+
+ return if S->{'bind_result'} eq 'skipped';
+
+ my $func = 'listen_for_changes';
+ my %args = ();
+
+ $args{'-basedn'} = sprintf('%s,%s', $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ $args{'-scope'} = LDAP_SCOPE_SUBTREE;
+ $args{'-filter'} = '(objectClass=*)';
+ $args{'-cookie'} = $TestConfig{'syncrepl'}{'cookie_dir'} . "syncrepl.$$.cookie";
+
+# open(COOKIE, ">" . $args{'-cookie'});
+# close(COOKIE);
+
+ S->{'listen changes_result'} = S->{'object'}->$func(%args);
+ S->{'object'}->next_changed_entries(S->{'listen changes_result'}, 0, 1);
+
+};
+
+Then qr/the changes were successfully notified/i, sub {
+ my $expected_container_dn = sprintf('%s,%s,%s', $TestConfig{'data'}{'container_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+ my $expected_entry_dn = sprintf('%s,%s,%s', $TestConfig{'data'}{'entry_dn'}, $TestConfig{'data'}{'test_container_dn'}, $TestConfig{'ldap'}{'base_dn'});
+
+ my $seen_expected = 0;
+
+ my $timeout_start = time();
+ my $timeout_length = 5;
+
+ while(!$seen_expected) {
+ if ((time() - $timeout_start) > $timeout_length) { last; }
+
+ while(my @entries = S->{'object'}->next_changed_entries(S->{'listen changes_result'}, 0, 1)) {
+ foreach my $entry (@entries) {
+
+ my $entry_dn = S->{'object'}->get_dn($entry->{'entry'});
+
+ if (lc($entry_dn) eq lc($expected_container_dn) || lc($entry_dn) eq lc($expected_entry_dn)) {
+ $seen_expected = 1;
+ last;
+ }
+ }
+ }
+ }
+
+ ok($seen_expected, 'Have we seen a notification for an expected DN?');
+
+ my %args;
+
+ my $asn = Convert::ASN1->new();
+ $asn->prepare(<<ASN);
+cancelRequestValue ::= SEQUENCE {
+ cancelID INTEGER
+}
+ASN
+
+ $args{'-oid'} = '1.3.6.1.1.8';
+ $args{'-result'} = \%{S->{'cancel_result code'}};
+ $args{'-berval'} = $asn->encode(cancelID => S->{'listen changes_result'});
+
+ my $cancel_status = S->{'object'}->extended_operation_s(%args);
+ is(ldap_err2string($cancel_status), ldap_err2string(LDAP_SUCCESS), 'Was cancelling the sync successful?');
+ S->{'object'}->{'entry'} = 0;
+};
+
+1;
diff --git a/t/features/step_definitions/whoami_steps.pl b/t/features/step_definitions/whoami_steps.pl
new file mode 100644
index 0000000..e78bb79
--- /dev/null
+++ b/t/features/step_definitions/whoami_steps.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::BDD::Cucumber::StepFile;
+
+our %TestConfig = %main::TestConfig;
+
+When qr/I\'ve (asynchronously )?queried the directory for my identity/, sub {
+ my $async = $1 ? 1 : 0;
+
+ S->{'identity_authzid'} = undef;
+ S->{'identity_result'} = "skipped";
+
+ return if S->{"bind_result"} eq "skipped";
+
+ my $func = "whoami_s";
+ my %args = ();
+
+ if ($async) {
+ $func = "whoami";
+ } else {
+ %args = ('-authzid' => \S->{'identity_authzid'});
+ }
+ S->{'identity_async'} = $async;
+
+ S->{'identity_result'} = S->{'object'}->$func(%args);
+};
+
+Then qr/the identity matches/, sub {
+ SKIP: {
+
+ skip(S->{'bind_type'} . " authentication disabled in t/test-config.pl", 1) if S->{"bind_result"} eq "skipped";
+
+ my ($got, $expected);
+
+ if (S->{'identity_async'}) {
+ $got = S->{'object'}->parse_whoami(S->{'identity_result_id'});
+ } else {
+ $got = S->{'identity_authzid'};
+ }
+
+ S->{'identity_got'} = $got;
+
+ if (S->{'bind_type'} eq "anonymous") {
+ $expected = "";
+ } elsif (S->{'bind_type'} eq "simple") {
+ my ($attr, $value) = split(/:/, $got);
+
+ $got = $value;
+ $expected = $TestConfig{'ldap'}{'bind_types'}{'simple'}{'bind_dn'};
+ } elsif (S->{'bind_type'} eq "sasl") {
+ my ($attr, $value) = split(/:/, $got);
+
+ $got = $value;
+ $expected = $TestConfig{'ldap'}{'bind_types'}{'sasl'}{'identity'};
+ }
+
+ is(lc($got), lc($expected), "Does expected identity match received identity?");
+ }
+};
+
+1;
diff --git a/t/features/syncrepl.feature b/t/features/syncrepl.feature
new file mode 100644
index 0000000..3c6b9d9
--- /dev/null
+++ b/t/features/syncrepl.feature
@@ -0,0 +1,21 @@
+Feature: Listening for changes within the directory with syncrepl
+ As a OpenLDAP directory consumer
+ I want to ensure that I can be notified of changes to entries within the directory
+ In order to act quickly on changes
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can listen for changes within the directory
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with default authentication to the directory
+ And a test container has been created
+ And I've started listening for changes within the directory
+ And I've added a new entry to the directory
+ And I've added a new container to the directory
+ And I've deleted the new entry from the directory
+ Then the new entry result is LDAP_SUCCESS
+ And the new container result is LDAP_SUCCESS
+ And the delete entry result is LDAP_SUCCESS
+ And the changes were successfully notified
+ And the test container has been deleted
diff --git a/t/features/whoami.feature b/t/features/whoami.feature
new file mode 100644
index 0000000..892fa04
--- /dev/null
+++ b/t/features/whoami.feature
@@ -0,0 +1,61 @@
+Feature: Querying the directory for my identity
+ As a directory consumer
+ I want to ensure that I can retrieve my identity
+ In order to determine my DN when using a non-simple authentication
+
+ Background:
+ Given a usable Net::LDAPapi class
+
+ Scenario: Can query identity with anonymous authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with anonymous authentication to the directory
+ And I've queried the directory for my identity
+ Then the bind result is LDAP_SUCCESS
+ And the identity result is LDAP_SUCCESS
+ And the identity matches
+
+ Scenario: Can query identity with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've bound with simple authentication to the directory
+ And I've queried the directory for my identity
+ Then the bind result is LDAP_SUCCESS
+ And the identity result is LDAP_SUCCESS
+ And the identity matches
+
+ Scenario: Can query identity with sasl authentication
+ Given a Net::LDAPapi object that has been connected to the ldapi LDAP server
+ When I've bound with sasl authentication to the directory
+ And I've queried the directory for my identity
+ Then the bind result is LDAP_SUCCESS
+ And the identity result is LDAP_SUCCESS
+ And the identity matches
+
+ Scenario: Can asynchronously query identity with anonymous authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with anonymous authentication to the directory
+ And I've asynchronously queried the directory for my identity
+ Then the bind result message type is LDAP_RES_BIND
+ And the bind result is LDAP_SUCCESS
+ And after waiting for all results, the identity result message type is LDAP_RES_EXTENDED
+ And the identity result is LDAP_SUCCESS
+ And the identity matches
+
+ Scenario: Can asynchronously query identity with simple authentication
+ Given a Net::LDAPapi object that has been connected to the LDAP server
+ When I've asynchronously bound with simple authentication to the directory
+ And I've asynchronously queried the directory for my identity
+ Then the bind result message type is LDAP_RES_BIND
+ And the bind result is LDAP_SUCCESS
+ And after waiting for all results, the identity result message type is LDAP_RES_EXTENDED
+ And the identity result is LDAP_SUCCESS
+ And the identity matches
+
+# Scenario: Can asynchronously query identity with sasl authentication
+# Given a Net::LDAPapi object that has been connected to the ldapi LDAP server
+# When I've asynchronously bound with sasl authentication to the directory
+# And I've asynchronously queried the directory for my identity
+# Then the bind result message type is LDAP_RES_BIND
+# And the bind result is LDAP_SUCCESS
+# And after waiting for all results, the identity result message type is LDAP_RES_EXTENDED
+# And the identity result is LDAP_SUCCESS
+# And the identity matches
diff --git a/t/test-config.pl b/t/test-config.pl
new file mode 100644
index 0000000..83fa8e6
--- /dev/null
+++ b/t/test-config.pl
@@ -0,0 +1,129 @@
+# Developer tests require:
+# Test::More
+# Test::BDD::Cucumber
+our $RunDeveloperTests = 0;
+
+# Default config.
+# If you're a developer of Net::LDAPapi or are likely to have multiple trees that share common test config,
+# then you should override in ~/.net-ldapapi-test-config.conf (See below)
+our %TestConfig = (
+ 'ldap' => {
+ 'server' => {
+ 'tcp' => {
+ '-host' => 'localhost',
+ '-port' => 389,
+ },
+ 'ldapi' => {
+ '-url' => 'ldapi:///',
+ '-debug' => 1
+ }
+ },
+ 'base_dn' => 'dc=example,dc=com',
+ 'bind_types' => {
+ 'anonymous' => {
+ 'enabled' => 1,
+ },
+ 'simple' => {
+ 'enabled' => 1,
+ 'bind_dn' => 'cn=admin,dc=example,dc=com',
+ 'bind_pw' => 'password',
+ },
+ 'sasl' => {
+ 'enabled' => 1,
+ 'sasl_parms' => {
+ '-mech' => 'EXTERNAL',
+ },
+ 'identity' => "gidNumber=" . $< . "+uidNumber=" . (split(/ /, "$("))[0] . ",cn=peercred,cn=external,cn=auth"
+ }
+ },
+ 'default_server' => 'tcp',
+ 'default_bind_type' => 'simple',
+ },
+ 'search' => {
+ 'filter' => "sn=Last",
+ 'count' => 1,
+ },
+ 'data' => {
+ 'test_container_attributes' => {
+ 'objectClass' => ['top', 'organizationalUnit'],
+ 'ou' => 'Test Container',
+ },
+ 'container_attributes' => {
+ 'objectClass' => ['top', 'organizationalUnit'],
+ 'ou' => 'Test - Add Container',
+ },
+ 'entry_attributes' => {
+ 'objectClass' => ['top', 'person' ,'organizationalPerson', 'inetOrgPerson'],
+ 'cn' => 'Test - Add Entry',
+ 'sn' => 'Entry',
+ 'givenName' => 'Test - Add',
+ },
+ 'test_container_dn' => 'ou=Test Container',
+ 'container_dn' => 'ou=Test - Add Container',
+ 'entry_dn' => 'cn=Test - Add Entry',
+ },
+ 'rename' => {
+ 'dn' => 'cn=Test - Add Entry',
+ 'new_rdn' => 'cn=Test - Add Entry',
+ 'new_super' => 'ou=Test - Add Container'
+ },
+ 'modify' => {
+ 'new_attribute' => {
+ 'title' => { 'a' => ['New Test Title'] }
+ },
+ 'modify_attribute' => {
+ 'title' => { 'r' => ['Modified Test Title'] }
+ },
+ 'remove_attribute' => {
+ 'title' => ''
+ },
+ },
+ 'syncrepl' => {
+ 'enabled' => 1,
+ 'cookie_dir' => '/tmp/'
+ },
+ 'server_controls' => {
+ 'sss' => [
+ {
+ 'attributeType' => 'sn',
+ 'orderingRule' => '2.5.13.3',
+ 'reverseOrder' => 1
+ },
+ ],
+ 'vlv' => {
+ 'beforeCount' => 0,
+ 'afterCount' => 3,
+ 'target' => {
+ 'byOffset' => {
+ 'offset' => 1,
+ 'contentCount' => 0
+ }
+ }
+ },
+ },
+ 'compare' => {
+ 'entry_attribute' => 'cn',
+ 'compare_attribute' => 'ou'
+ }
+);
+
+
+# Allow overrides from outside the source tree.
+# This is a standard Perl file. Example below.
+if ( -e $ENV{'HOME'} . '/.net-ldapapi-test-config.conf') {
+ require $ENV{'HOME'} . '/.net-ldapapi-test-config.conf';
+}
+
+1;
+__END__
+
+$RunDeveloperTests = 1;
+
+$TestConfig{'ldap'}{'base_dn'} = "o=Test Data,c=NZ";
+$TestConfig{'ldap'}{'bind_types'}{'simple'}{'bind_dn'} = "cn=admin,o=Test Data,c=NZ";
+$TestConfig{'ldap'}{'bind_types'}{'simple'}{'bind_pw'} = "password";
+
+$TestConfig{'search'}{'filter'} = "sn=O'Donnell";
+
+
+1;
diff --git a/test.pl b/test.pl
index 637e978..7deb1a6 100644
--- a/test.pl
+++ b/test.pl
@@ -6,7 +6,7 @@
# Change 1..1 below to 1..last_test_to_print .
# (It may become useful if the test is moved to ./t subdirectory.)
-BEGIN { $| = 1; print "1..8\n"; }
+BEGIN { $| = 1; print "1..10\n"; }
END {print "modinit - not ok\n" unless $loaded;}
use Net::LDAPapi;
$loaded = 1;
@@ -57,6 +57,35 @@ if ($ld->bind_s != LDAP_SUCCESS)
print "bind - ok\n";
##
+## ldap_whoami_s
+##
+
+$id = '';
+
+if ($ld->whoami_s(\$id) != LDAP_SUCCESS)
+{
+ $ld->perror("whoami_s");
+ print "whoami - not ok\n";
+ exit -1;
+}
+print "whoami - ok\n";
+
+##
+## ldap_extended_operation_s
+##
+
+%result = ();
+
+if ($ld->extended_operation_s(-oid => "1.3.6.1.4.1.4203.1.11.3", -result => \%result) != LDAP_SUCCESS)
+{
+ $ld->perror("ldap_extended_operation_s");
+ print "ldap_extended_operation - not ok\n";
+ exit -1;
+}
+print "ldap_extended_operation - ok\n";
+
+
+##
## ldap_search_s - Synchronous Search
##
@@ -122,6 +151,7 @@ print "count - ok\n";
}
+
##
## Unbind LDAP Connection
##
diff --git a/typemap b/typemap
index 4be4277..db5fd6e 100644
--- a/typemap
+++ b/typemap
@@ -16,6 +16,7 @@ BerElement * T_PTR
BerElement ** T_PTR
LDAPVersion * T_PTR
LDAPMod * T_PTR
+LDAPMod ** T_PTR
struct berval * T_PTR
struct berval ** T_PTR
struct timeval * T_PTR