From 191a1196060270243502288e4d06b3585ea95968 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Fri, 24 Mar 2023 02:48:12 +0100 Subject: [PATCH] Add tests on Ldap class and fix some related bugs --- composer.json | 4 +- phpstan.neon | 6 + src/Entry.php | 2 - src/Ldap.php | 41 +- src/Schema.php | 2 +- tests/LdapTest.php | 1293 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1327 insertions(+), 21 deletions(-) create mode 100644 tests/LdapTest.php diff --git a/composer.json b/composer.json index 4dce187..e1bf9ea 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,9 @@ "description": "Object oriented interface for searching and manipulating LDAP entries & filters", "require-dev": { "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9" + "phpunit/phpunit": "^9", + "lstrojny/phpunit-function-mocker": "^1.1", + "mockery/mockery": "^1.5" }, "authors": [ { diff --git a/phpstan.neon b/phpstan.neon index 6e30a4e..b770283 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,3 +10,9 @@ parameters: - message: "#Method .*::test.*\\(\\) has no return type specified\\.#" path: tests/* + - + message: "#Call to an undefined method Mockery\\\\ExpectationInterface|Mockery\\\\HigherOrderMessage::once\\(\\)\\.#" + path: tests/* + - + message: "#Call to an undefined method PHPUnit\\\\Extension\\\\FunctionMocker::expects\\(\\)\\.#" + path: tests/* diff --git a/src/Entry.php b/src/Entry.php index 7dd7140..139077c 100644 --- a/src/Entry.php +++ b/src/Entry.php @@ -416,8 +416,6 @@ class Entry { if ($this->ldap) // @phpstan-ignore-next-line return $this->ldap->error($error); - // @phpstan-ignore-next-line - error_log($error); throw new LdapException($error); // @phpstan-ignore-line } diff --git a/src/Ldap.php b/src/Ldap.php index 02fb9d2..1a1be0e 100644 --- a/src/Ldap.php +++ b/src/Ldap.php @@ -21,6 +21,8 @@ namespace EesyLDAP; * @property-read string $scope * @property-read bool $raise_on_error * @property-read bool $use_schema + * @property-read array $config_aliases + * @property-read array $default_config * @property-read Schema|false $schema */ class Ldap { @@ -180,7 +182,7 @@ class Ldap { break; case 'filter': if (is_string($value)) - $value = new Filter($value); + $value = Filter::parse($value); elseif (!$value instanceof Filter) { $this -> error( "Invalid filter found in configuration: expect to be a string or a \EesyLDAP\Filter ". @@ -199,8 +201,6 @@ class Ldap { ); continue 2; } - - } $this -> config[$key] = $value; } @@ -238,7 +238,7 @@ class Ldap { public function connect($raise=null, $bind_dn=null, $bind_password=null) { if ($this -> _link) return true; - if (!self :: check_ldap_extension($raise)) + if (!$this -> check_ldap_extension($raise)) return false; foreach($this -> hosts as $host) { @@ -264,7 +264,7 @@ class Ldap { continue; } - if (!$this -> bind($bind_dn, $bind_password)) { + if (!$this -> bind($bind_dn, $bind_password, false)) { $this -> close(); $this -> error( "Fail to bind on %s", false); @@ -277,7 +277,7 @@ class Ldap { if (in_array($option, $this -> required_options)) { $this -> close(); $this -> error('Fail to set option %s on %s', false, $option, $host); - continue; + continue 2; } } } @@ -350,8 +350,8 @@ class Ldap { return $this -> error("%s: unkown error", $raise, $prefix); $err = $this->_link?$this->_link->err2str($errno):false; if (!$err) - return $this -> error("%s: error #%s", $raise, $errno); - return $this -> error("%s: %s (#%s)", $raise, $err, $errno); + return $this -> error("%s: error #%s", $raise, $prefix, $errno); + return $this -> error("%s: %s (#%s)", $raise, $prefix, $err, $errno); } /** @@ -366,8 +366,6 @@ class Ldap { if ($extra_args) $error = call_user_func_array('sprintf', array_merge(array($error), $extra_args)); // Note: sprintf always return string - // @phpstan-ignore-next-line - error_log($error); if (is_null($raise)) $raise = $this -> raise_on_error; if ($raise) @@ -449,14 +447,17 @@ class Ldap { * - attributes: Array of attribute names, which the entries in the expected result should contain. * It is good practice to limit this to just the ones you need. * - * @param string|null $base Base DN of the search (optional, default: null == use config) * @param string|null $filter Filter of the search (optional, default: null == use config) + * @param string|Entry|null $base Base DN of the search (optional, default: null == use config) + * @param string|null $scope Scope of the search (optional, default: null == use params or config) + * @param array|null $attributes Expected attributes returned by the search (optional, + * default: null == use params or config) * @param array|null $params Other optional parameters of the search (optional, default: null == use config) * @param bool|null $raise See error() (optional, default: null) * @return array|false Array of Entry object with DN as key, or False in case of error * @throws LdapException */ - public function search($base=null, $filter=null, $params=null, $raise=null) { + public function search($filter=null, $base=null, $scope=null, $attributes=null, $params=null, $raise=null) { if (!$this -> _link) return $this -> error("Can't search: no LDAP link"); if (is_null($base)) @@ -469,17 +470,22 @@ class Ldap { $filter = $filter->as_string(); // convert Filter object as string // Adjust search function to expected scope - $scope = self :: get_param($params, 'scope', $this->scope, 'string'); + $scope = $scope?$scope:self :: get_param($params, 'scope', $this->scope, 'string'); if (!is_string($scope) || !array_key_exists($scope, self :: $search_function_by_scope)) return $this -> error("Invalid scope '%s' specified", $raise, $scope); + // Compute expected attributes + $attributes = $attributes?$attributes:self :: get_param($params, 'attributes', array(), 'array'); + if (!is_array($attributes)) + return $this -> error("Invalid expected attributes specified: must be an array", $raise); + // Run the search $search = @call_user_func( // @phpstan-ignore-next-line array($this->_link, self :: $search_function_by_scope[$scope]), $base, $filter, - self :: get_param($params, 'attributes', array(), 'array'), + $attributes, self :: get_param($params, 'attrsonly', false, 'bool')?1:0, self :: get_param($params, 'sizelimit', 0, 'int'), self :: get_param($params, 'timelimit', 0, 'int') @@ -521,16 +527,17 @@ class Ldap { * * @param string $dn DN of the expected entry * @param string|null $filter Filter of the search (optional, default: null == use config) + * @param array|null $attributes Expected attributes returned by the search (optional, + * default: null == use params or config) * @param array|null $params Other optional parameters of the search (optional, * default: null == use config, see search()) * @param bool|null $raise See error() (optional, default: null) * @return Entry|false The requested Entry object, or False in case of error * @throws LdapException */ - public function get_entry($dn, $filter=null, $params=null, $raise=null) { + public function get_entry($dn, $filter=null, $attributes=null, $params=null, $raise=null) { $params = is_array($params)?$params:array(); - $params['scope'] = 'base'; - $entries = $this -> search($dn, $filter, $params, $raise); + $entries = $this -> search($filter, $dn, 'base', $attributes, $params, $raise); if (is_array($entries) && count($entries) == 1) return array_pop($entries); return false; diff --git a/src/Schema.php b/src/Schema.php index 8815530..4d6769d 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -100,7 +100,7 @@ class Schema { public static function load(&$ldap, $raise=null) { $entry = $ldap->get_entry( 'cn=SubSchema', '(objectclass=*)', - array('attributes' => array_keys(self :: $attributes_to_entry_types)) + array_keys(self :: $attributes_to_entry_types) ); if (!$entry instanceof Entry) return $ldap->error('Fail to load cn=SubSchema entry', $raise); diff --git a/tests/LdapTest.php b/tests/LdapTest.php new file mode 100644 index 0000000..6934f46 --- /dev/null +++ b/tests/LdapTest.php @@ -0,0 +1,1293 @@ +func = FunctionMocker::start($this, 'EesyLDAP') + ->mockFunction('extension_loaded') + ->getMock(); + } + + /** + * Set config with invalid config data + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + // @phpstan-ignore-next-line + $ldap->set_config(false); + } + + /** + * Set config with invalid parameter + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigInvalidParameter() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('fake' => 'value')); + } + + /** + * Set config + * @covers \EesyLDAP\Ldap::set_config + * @covers \EesyLDAP\Filter + */ + public function testSetConfig() { + $ldap = new Ldap(null, false); + $config = array( + 'hosts' => array('ldap.example.com'), + 'port' => 666, + 'version' => 2, + 'starttls' => true, + 'bind_dn' => 'cn=admin,o=example', + 'bind_password' => 'secret', + 'basedn' => 'o=example', + 'options' => array('LDAP_OPT_NETWORK_TIMEOUT' => 30), + 'filter' => Filter::parse('(objectClass=person)'), + 'scope' => 'one', + 'raise_on_error' => false, + 'use_schema' => true, + ); + $ldap->set_config($config); + foreach($config as $key => $expected_value) + $this->assertSame($expected_value, $ldap->get_config($key)); + } + + /** + * Set config using aliases + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigWithAlias() { + $ldap = new Ldap(null, false); + $config = array( + 'host' => array('ldap.example.com'), + 'binddn' => 'cn=admin,o=example', + 'bindpw' => 'secret', + ); + $ldap->set_config($config); + foreach($config as $key => $expected_value) + $this->assertSame($expected_value, $ldap->get_config($key)); + } + + /** + * Set config hosts as string + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigHostsString() { + $ldap = new Ldap(null, false); + $hosts = array('host1', 'host2'); + $ldap->set_config(array('hosts' => implode(',', $hosts))); + $this->assertSame($hosts, $ldap->get_config('hosts')); + } + + /** + * Set config hosts as array + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigHostsArray() { + $ldap = new Ldap(null, false); + $hosts = array('host1', 'host2'); + $ldap->set_config(array('hosts' => $hosts)); + $this->assertSame($hosts, $ldap->get_config('hosts')); + } + + /** + * Set config empty host + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigEmptyHost() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('hosts' => 'host1,')); + } + + /** + * Set config invalid hosts + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigInvalidHosts() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('hosts' => null)); + } + + /** + * Set config port + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigPort() { + $ldap = new Ldap(null, false); + $port = 666; + $ldap->set_config(array('port' => $port)); + $this->assertSame($port, $ldap->get_config('port')); + } + + /** + * Set config version + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigVersion() { + $ldap = new Ldap(null, false); + $version = 2; + $ldap->set_config(array('version' => $version)); + $this->assertSame($version, $ldap->get_config('version')); + } + + /** + * Set config starttls + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigStartTLS() { + $ldap = new Ldap(null, false); + $starttls = true; + $ldap->set_config(array('starttls' => $starttls)); + $this->assertSame($starttls, $ldap->get_config('starttls')); + } + + /** + * Set config options + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigOptions() { + $ldap = new Ldap(null, false); + $options = array('LDAP_OPT_NETWORK_TIMEOUT' => 30); + $ldap->set_config(array('options' => $options)); + $this->assertSame($options, $ldap->get_config('options')); + } + + /** + * Set config options with invalid value + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigOptionsInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('options' => null)); + } + + /** + * Set config required options + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigRequiredOptions() { + $ldap = new Ldap(null, false); + $required_options = array('LDAP_OPT_NETWORK_TIMEOUT'); + $ldap->set_config(array('required_options' => $required_options)); + $this->assertSame($required_options, $ldap->get_config('required_options')); + } + + /** + * Set config required options as true + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigRequiredOptionsTrue() { + $ldap = new Ldap(null, false); + $options = array('LDAP_OPT_NETWORK_TIMEOUT' => 30); + $ldap->set_config(array('options' => $options)); + $ldap->set_config(array('required_options' => true)); + $this->assertSame(array_keys($options), $ldap->get_config('required_options')); + } + + /** + * Set config required options as false + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigRequiredOptionsFalse() { + $ldap = new Ldap(null, false); + $options = array('LDAP_OPT_NETWORK_TIMEOUT' => 30); + $ldap->set_config(array('options' => $options)); + $ldap->set_config(array('required_options' => false)); + $this->assertSame(array(), $ldap->get_config('required_options')); + } + + /** + * Set config required options with invalid value + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigRequiredOptionsInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('required_options' => null)); + } + + /** + * Set config filter as string + * @covers \EesyLDAP\Ldap::set_config + * @covers \EesyLDAP\Filter + */ + public function testSetConfigFilterString() { + $ldap = new Ldap(null, false); + $filter = '(uid=test)'; + $ldap->set_config(array('filter' => $filter)); + $this->assertEquals(Filter::parse($filter), $ldap->get_config('filter')); + } + + /** + * Set config filter as object + * @covers \EesyLDAP\Ldap::set_config + * @covers \EesyLDAP\Filter + */ + public function testSetConfigFilterObject() { + $ldap = new Ldap(null, false); + $filter = Filter::parse('(uid=test)'); + $ldap->set_config(array('filter' => $filter)); + $this->assertSame($filter, $ldap->get_config('filter')); + } + + /** + * Set config filter with invalid value + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigFilterInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('filter' => null)); + } + + /** + * Set config scope + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigScope() { + $ldap = new Ldap(null, false); + $scopes = array('one', 'base', 'sub'); + foreach ($scopes as $scope) { + $ldap->set_config(array('scope' => $scope)); + $this->assertSame($scope, $ldap->get_config('scope')); + } + } + + /** + * Set config scope with invalid value + * @covers \EesyLDAP\Ldap::set_config + */ + public function testSetConfigScopeInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_config(array('scope' => 'fake')); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnect() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'starttls' => true, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('start_tls') + ->once() + ->with() + ->andReturn(true); + + $ldap = new Ldap($config); + } + + /** + * @covers \EesyLDAP\Ldap::connect + */ + public function testConnectWhenAlreadyConnected() { + $ldap = new Ldap(null, false); + $reflection = new ReflectionClass($ldap); + $link_property = $reflection->getProperty('_link'); + $link_property->setAccessible(true); + $link_property->setValue($ldap, true); + $this -> assertTrue($ldap->connect()); + } + + /** + * @covers \EesyLDAP\Ldap::connect + */ + public function testConnectNoLdapExtension() { + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['check_ldap_extension']) + ->getMock(); + $ldap->expects($this->once()) + ->method('check_ldap_extension') + ->willReturn(false); + $this -> assertFalse($ldap->connect()); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectFailure() { + $config = array( + 'host' => 'ldap1', 'port' => 389, + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(false); + $this->expectException(LdapException::class); + $ldap = new Ldap($config); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectStartTLSFailure() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'starttls' => true, + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('start_tls') + ->once() + ->with() + ->andReturn(false); + $link->shouldReceive('close') + ->once() + ->with(); + $this->expectException(LdapException::class); + $ldap = new Ldap($config); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectSetVersionFailure() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(false); + $link->shouldReceive('errno') + ->once() + ->with() + ->andReturn(10); + $link->shouldReceive('err2str') + ->once() + ->with(10) + ->andReturn('oups'); + $link->shouldReceive('close') + ->once() + ->with(); + $this->expectException(LdapException::class); + $ldap = new Ldap($config); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectBindFailure() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(false); + $link->shouldReceive('errno') + ->once() + ->with() + ->andReturn(false); + $link->shouldReceive('close') + ->with(); + $this->expectException(LdapException::class); + $ldap = new Ldap($config); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::connect + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testConnectRequiredOptionFailure() { + $option_name = 'LDAP_OPT_NETWORK_TIMEOUT'; + $option_value = 30; + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret', + 'options' => array($option_name => $option_value), + 'required_options' => array($option_name) + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(constant($option_name), $option_value) + ->andReturn(false); + $link->shouldReceive('errno') + ->once() + ->with() + ->andReturn(false); + $link->shouldReceive('close') + ->with(); + $this->expectException(LdapException::class); + $ldap = new Ldap($config); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::set_option + */ + public function testSetOptionNoLink() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->set_option('LDAP_OPT_NETWORK_TIMEOUT', 30); + } + + /** + * @covers \EesyLDAP\Ldap::set_option + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSetOptionInvalid() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->andReturn(true); + $ldap = new Ldap($config); + $this->expectException(LdapException::class); + $ldap->set_option('LDAP_FAKE_OPT', 30); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::__get + */ + public function testGetConfigAliases() { + $ldap = new Ldap(null, false); + $this->assertIsArray($ldap->config_aliases); + } + + /** + * @covers \EesyLDAP\Ldap::__get + */ + public function testGetDefaultConfig() { + $ldap = new Ldap(null, false); + $this->assertIsArray($ldap->default_config); + } + + /** + * @covers \EesyLDAP\Ldap::__get + */ + public function testGetSchemaWhenDisable() { + $ldap = new Ldap(array('use_schema' => false), false); + $this->assertFalse($ldap->schema); + } + + /** + * @covers \EesyLDAP\Ldap::__get + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetSchema() { + $ldap = new Ldap(array('use_schema' => true), false); + $schema = Mockery::mock('overload:EesyLDAP\Schema'); + $schema->shouldReceive('load') + ->once() + ->with($ldap) + ->andSet('loaded', true) + ->andReturn($schema); + $this->assertSame($schema, $ldap->__get('schema')); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::__get + */ + public function testGetInvalidProperty() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->__get('invalid'); + } + + /** + * @covers \EesyLDAP\Ldap::get_config + */ + public function testGetConfigInvalid() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->get_config('invalid'); + } + + /** + * @covers \EesyLDAP\Ldap::get_config + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetConfigFilterFromString() { + $ldap = new Ldap(null, false); + $filter = Mockery::mock('overload:EesyLDAP\Filter'); + $filter->shouldReceive('parse') + ->once() + ->with($ldap->default_config['filter']) + ->andReturn('success'); + $this->assertEquals('success', $ldap->get_config('filter')); + } + + /** + * @covers \EesyLDAP\Ldap::get_config + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchWithoutParameter() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret', + 'basedn' => 'o=example', 'filter' => '(objectClass=*)' + ); + $result_ref = 'success'; + $raw_result = [ + 'count' => 1, + 0 => [ + 'objectclass' => [ + 'count' => 2, + 0 => 'top', + 1 => 'inetOrgPerson', + ], + 0 => 'objectclass', + 'cn' => [ + 'count' => 1, + 0 => 'test', + ], + 1 => 'cn', + 'count' => 2, + 'dn' => 'cn=test', + ], + ]; + $expected_result = array( + 'cn=test' => array( + 'objectclass' => array('top', 'inetOrgPerson'), + 'cn' => array('test'), + ), + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('search') + ->once() + ->with($config['basedn'], $config['filter'], null, 0, 0, 0) + ->andReturn($result_ref); + $link->shouldReceive('get_entries') + ->once() + ->with($result_ref) + ->andReturn($raw_result); + $ldap = new Ldap($config); + $result = $ldap->search(); + $this->assertIsArray($result); + $this->assertCount(count($expected_result), $result); + $this->assertContainsOnlyInstancesOf(Entry::class, $result); + $dn = key($result); + $this->assertEquals(key($expected_result), $dn); + foreach($expected_result[$dn] as $attr => $values) + $this->assertEquals($values, $result[$dn]->get_raw_values($attr)); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::get_config + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchWithParameters() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $filter = '(cn=test)'; + $base = 'o=example'; + $scope = 'one'; + $attributes = ['cn', 'objectclass']; + $params = [ + 'attrsonly' => 1, + 'sizelimit' => 10, + 'timelimit' => 60, + ]; + $result_ref = 'success'; + $raw_result = [ + 'count' => 1, + 0 => [ + 'objectclass' => [ + 'count' => 2, + 0 => 'top', + 1 => 'inetOrgPerson', + ], + 0 => 'objectclass', + 'cn' => [ + 'count' => 1, + 0 => 'test', + ], + 1 => 'cn', + 'count' => 2, + 'dn' => 'cn=test', + ], + ]; + $expected_result = array( + 'cn=test' => array( + 'objectclass' => array('top', 'inetOrgPerson'), + 'cn' => array('test'), + ), + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('list') + ->once() + ->with( + $base, $filter, $attributes, $params['attrsonly'], $params['sizelimit'], + $params['timelimit']) + ->andReturn($result_ref); + $link->shouldReceive('get_entries') + ->once() + ->with($result_ref) + ->andReturn($raw_result); + $ldap = new Ldap($config); + $result = $ldap->search($filter, $base, $scope, $attributes, $params); + $this->assertIsArray($result); + $this->assertCount(count($expected_result), $result); + $this->assertContainsOnlyInstancesOf(Entry::class, $result); + $dn = key($result); + $this->assertEquals(key($expected_result), $dn); + foreach($expected_result[$dn] as $attr => $values) + $this->assertEquals($values, $result[$dn]->get_raw_values($attr)); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::get_config + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchWithEntryAsBaseDN() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $filter = '(cn=test)'; + $dn = 'cn=test'; + $base = new Entry($dn); + $scope = 'sub'; + $result_ref = 'success'; + $raw_result = [ + 'count' => 1, + 0 => [ + 'objectclass' => [ + 'count' => 2, + 0 => 'top', + 1 => 'inetOrgPerson', + ], + 0 => 'objectclass', + 'cn' => [ + 'count' => 1, + 0 => 'test', + ], + 1 => 'cn', + 'count' => 2, + 'dn' => 'cn=test', + ], + ]; + $expected_result = array( + 'cn=test' => array( + 'objectclass' => array('top', 'inetOrgPerson'), + 'cn' => array('test'), + ), + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('search') + ->once() + ->with($dn, $filter, array(), 0, 0, 0) + ->andReturn($result_ref); + $link->shouldReceive('get_entries') + ->once() + ->with($result_ref) + ->andReturn($raw_result); + $ldap = new Ldap($config); + $result = $ldap->search($filter, $base, $scope); + $this->assertIsArray($result); + $this->assertCount(count($expected_result), $result); + $this->assertContainsOnlyInstancesOf(Entry::class, $result); + Mockery::close(); + } + + /** + * @covers \EesyLDAP\Ldap::search + */ + public function testSearchNoLink() { + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->search(); + } + + /** + * @covers \EesyLDAP\Ldap::search + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchInvalidScope() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $ldap = new Ldap($config); + $this->expectException(LdapException::class); + $ldap->search('cn=test', null, 'bad_scope'); + } + + /** + * @covers \EesyLDAP\Ldap::search + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchInvalidAttributes() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $ldap = new Ldap($config); + $this->expectException(LdapException::class); + // @phpstan-ignore-next-line + $ldap->search('cn=test', null, null, true); + } + + /** + * @covers \EesyLDAP\Ldap::search + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchBadResult() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('search') + ->once() + ->andReturn(false); + $link->shouldReceive('errno') + ->once() + ->with() + ->andReturn(false); + $ldap = new Ldap($config); + $this->expectException(LdapException::class); + $ldap->search(); + } + + /** + * @covers \EesyLDAP\Ldap::search + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testSearchBadEntries() { + $config = array( + 'host' => 'ldap1', 'port' => 389, 'version' => 2, + 'bind_dn' => 'cn=admin', 'bind_password' => 'secret' + ); + $result = 'success'; + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $link = Mockery::mock('overload:EesyLDAP\Link'); + $link->shouldReceive('connect') + ->once() + ->with($config['host'], $config['port']) + ->andReturn(true); + $link->shouldReceive('set_option') + ->once() + ->with(LDAP_OPT_PROTOCOL_VERSION, $config['version']) + ->andReturn(true); + $link->shouldReceive('bind') + ->once() + ->with($config['bind_dn'], $config['bind_password']) + ->andReturn(true); + $link->shouldReceive('search') + ->once() + ->andReturn($result); + $link->shouldReceive('get_entries') + ->once() + ->with($result) + ->andReturn(false); + $link->shouldReceive('errno') + ->once() + ->with() + ->andReturn(false); + $ldap = new Ldap($config); + $this->expectException(LdapException::class); + $ldap->search(); + } + + /** + * @covers \EesyLDAP\Ldap::get_entry + * @covers \EesyLDAP\Entry::__construct + */ + public function testGetEntry() { + $dn = 'cn=test'; + $filter = '(cn=test)'; + $attributes = array('cn'); + $params = array('attrsonly' => 1); + $raise = true; + $expected_entry = new Entry(); + $expected_result = array('cn=test' => $expected_entry); + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['search']) + ->getMock(); + $ldap->expects($this->once()) + ->method('search') + ->with($filter, $dn, 'base', $attributes, $params, $raise) + ->willReturn($expected_result); + $entry = $ldap->get_entry($dn, $filter, $attributes, $params, $raise); + $this->assertInstanceOf(Entry::class, $entry); + $this->assertSame($expected_entry, $entry); + } + + /** + * @covers \EesyLDAP\Ldap::get_entry + */ + public function testGetEntryInvalid() { + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['search']) + ->getMock(); + $ldap->expects($this->once()) + ->method('search') + ->willReturn(false); + $this->assertFalse($ldap->get_entry('o=example')); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParam() { + $params = ['test' => 'test']; + $ldap = new Ldap(null, false); + $this->assertEquals( + 'test', + self :: callProtectedMethod($ldap, 'get_param', array($params, 'test')) + ); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamDefault() { + $params = []; + $ldap = new Ldap(null, false); + $this->assertEquals( + 'default', + self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', 'default')) + ); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamBool() { + $params = ['test' => 1]; + $ldap = new Ldap(null, false); + $this->assertTrue( + self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'bool')) + ); + $this->assertTrue( + self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'boolean')) + ); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamInt() { + $params = ['test' => '10']; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'int')); + $this->assertIsInt($value); + $this->assertEquals(10, $value); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'integer')); + $this->assertIsInt($value); + $this->assertEquals(10, $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamFloat() { + $params = ['test' => '10.5']; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'float')); + $this->assertIsFloat($value); + $this->assertEquals(10.5, $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamString() { + $params = ['test' => '10.5']; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'str')); + $this->assertIsString($value); + $this->assertEquals('10.5', $value); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'string')); + $this->assertIsString($value); + $this->assertEquals('10.5', $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamArray() { + $params = ['test' => array('10.5')]; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'array')); + $this->assertIsArray($value); + $this->assertEquals(array('10.5'), $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamCastedArray() { + $params = ['test' => '10.5']; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'array')); + $this->assertIsArray($value); + $this->assertEquals(array('10.5'), $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testGetParamEmptyArray() { + $params = ['test' => null]; + $ldap = new Ldap(null, false); + $value = self :: callProtectedMethod($ldap, 'get_param', array($params, 'test', null, 'array')); + $this->assertIsArray($value); + $this->assertEquals(array(), $value); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testLogError() { + $prefix = 'Error %s occured'; + $prefix_var = 'test'; + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['error']) + ->getMock(); + $ldap->expects($this->once()) + ->method('error') + ->with("%s: unkown error", true, "Error test occured") + ->willReturn(false); + self :: callProtectedMethod($ldap, 'log_error', array($prefix, true, $prefix_var)); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testLogErrorWithLink() { + $prefix = 'Error %s occured'; + $prefix_var = 'test'; + $link = $this->getMockBuilder(Link::class) + ->disableOriginalConstructor() + ->onlyMethods(['errno', 'err2str']) + ->getMock(); + $link->expects($this->once()) + ->method('errno') + ->willReturn(10); + $link->expects($this->once()) + ->method('err2str') + ->with(10) + ->willReturn('error'); + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['error']) + ->getMock(); + $reflection = new ReflectionClass($ldap); + $link_property = $reflection->getProperty('_link'); + $link_property->setAccessible(true); + $link_property->setValue($ldap, $link); + $ldap->expects($this->once()) + ->method('error') + ->with("%s: %s (#%s)", true, "Error test occured", 'error', 10) + ->willReturn(false); + self :: callProtectedMethod($ldap, 'log_error', array($prefix, true, $prefix_var)); + } + + /** + * @covers \EesyLDAP\Ldap::get_param + */ + public function testLogErrorUnknown() { + $prefix = 'Error %s occured'; + $prefix_var = 'test'; + $link = $this->getMockBuilder(Link::class) + ->disableOriginalConstructor() + ->onlyMethods(['errno', 'err2str']) + ->getMock(); + $link->expects($this->once()) + ->method('errno') + ->willReturn(10); + $link->expects($this->once()) + ->method('err2str') + ->with(10) + ->willReturn(false); + $ldap = $this->getMockBuilder(Ldap::class) + ->disableOriginalConstructor() + ->onlyMethods(['error']) + ->getMock(); + $reflection = new ReflectionClass($ldap); + $link_property = $reflection->getProperty('_link'); + $link_property->setAccessible(true); + $link_property->setValue($ldap, $link); + $ldap->expects($this->once()) + ->method('error') + ->with('%s: error #%s', true, 'Error test occured', 10) + ->willReturn(false); + self :: callProtectedMethod($ldap, 'log_error', array($prefix, true, $prefix_var)); + } + + /** + * @covers \EesyLDAP\Ldap::check_ldap_extension + */ + public function testCheckLdapExtension() { + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(true)); + $ldap = new Ldap(null, false); + $this->assertTrue($ldap->check_ldap_extension()); + } + + /** + * @covers \EesyLDAP\Ldap::check_ldap_extension + */ + public function testCheckLdapExtensionNotInstalled() { + $this->func->expects($this->once()) + ->method('extension_loaded') + ->with('ldap') + ->will($this->returnValue(False)); + $ldap = new Ldap(null, false); + $this->expectException(LdapException::class); + $ldap->check_ldap_extension(); + } + + /** + * Helper to call protected method + * @param object $obj + * @param string $name + * @param array $args + * @return mixed + */ + public static function callProtectedMethod($obj, $name, $args) { + $class = new \ReflectionClass($obj); + $method = $class->getMethod($name); + $method->setAccessible(true); + return $method->invokeArgs($obj, $args); + } + +}