summaryrefslogtreecommitdiff
path: root/t
diff options
context:
space:
mode:
authorBill MacAllister <whm@stanford.edu>2016-08-14 00:54:43 -0700
committerBill MacAllister <whm@stanford.edu>2016-08-14 00:54:43 -0700
commit58f98fcb42c5b48c1686839f955a304bf2b844ca (patch)
treef003f769fec0dd825d62d989af1341e22770c80a /t
parentfc4d04018c3a527bb9e43f2fdec959f85270fc74 (diff)
Imported Upstream version 3.0.4
Diffstat (limited to 't')
-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
27 files changed, 1500 insertions, 0 deletions
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;