Mocking Symfony Ldap::create for unit tests - php

Recently I have been working on an LDAP authentication provider for MediaWiki. In my mind, I have been trying to tackle this issue for a number of days now and cannot come up with a solution.
Context
The way I have developed this plugin is to allow configuration of a number of servers to which we will connect. If we cannot connect to one server, we will try the next... And so on until all are exhausted.
To facilitate this, I have a function in my class that loops over the servers attempting a connection until one succeeds:
private function connect( LdapAuthenticationRequest $req ) {
$dn = $this->config->get( 'BindDN' )[$req->domain];
$pass = $this->config->get( 'BindPass' )[$req->domain];
$servers = $this->config->get( 'Servers' )[$req->domain];
$encryption = $this->config->get( 'EncryptionType' )[$req->domain];
if ( false === $dn ) {
$msgkey = 'ldapauth-attempt-bind-search';
$bind_with = [ null, null ];
} else {
$msgkey = 'ldapauth-attempt-bind-dn-search';
$bind_with = [ $dn, $pass ];
}
$message = new Message( $msgkey, [
'dn' => "{$dn}#{$req->domain}",
] );
$this->logger->info( $message->text() );
foreach ( $servers as $server ) {
if ( false === $server ) {
continue;
}
$ldap = Ldap::create( 'ext_ldap', [
'host' => $server,
'encryption' => $encryption
] );
// Attempt bind - on failure, throw an exception
try {
call_user_func_array( [ $ldap, 'bind' ], $bind_with );
$this->server = $server;
$this->encryption = $encryption;
// log successful bind
$msgkey = 'ldapauth-bind-success';
$message = wfMessage( $msgkey )->text();
$this->logger->info( $message );
return $ldap;
} catch ( SymException $e ) {
if ( false === $dn ) {
$msgkey = 'ldapauth-no-bind-search';
} else {
$msgkey = 'ldapauth-no-bind-dn-search';
}
$message = new Message( $msgkey, [
'dn' => "{$dn}#{$req->domain}",
] );
$message = $message->text();
$this->logger->info( $message );
$this->logger->debug( $e->getMessage() );
}
}
I have been trying to come up with a better way to do this, one that would permit me to better unit-test this class, but thus far I am drawing blanks.
A big part of the reason that I am stuck on this issue is that Symfony's LDAP adapter is essentially hard-coupled into my code, as the call to connect is a static call into Symfony's codebase. i.e. I cannot pass in a connector instance of some description that would then attempt the connection. Do I simply wrap Ldap::create with my own connection wrapper, perhaps?

Since you are using Symfony, I guess your best bet would be to inject LDap object using the framework's dependency injection. However I am not an expert in Symfony. So as a simple hack, I would do it like this :
private function connect($req)
{
$dn = $this->config->get('BindDN')[$req->domain];
$pass = $this->config->get('BindPass')[$req->domain];
$servers = $this->config->get('Servers')[$req->domain];
$encryption = $this->config->get('EncryptionType')[$req->domain];
if (false === $dn) {
$msgkey = 'ldapauth-attempt-bind-search';
$bind_with = [null, null];
} else {
$msgkey = 'ldapauth-attempt-bind-dn-search';
$bind_with = [$dn, $pass];
}
$message = new Message($msgkey, [
'dn' => "{$dn}#{$req->domain}",
]);
$this->logger->info($message->text());
foreach ($servers as $server) {
if (false === $server) {
continue;
}
$ldap = $this->createLDAPObject($server, $encryption);
// Attempt bind - on failure, throw an exception
try {
call_user_func_array([$ldap, 'bind'], $bind_with);
$this->server = $server;
$this->encryption = $encryption;
// log successful bind
$msgkey = 'ldapauth-bind-success';
$message = wfMessage($msgkey)->text();
$this->logger->info($message);
return $ldap;
} catch (SymException $e) {
if (false === $dn) {
$msgkey = 'ldapauth-no-bind-search';
} else {
$msgkey = 'ldapauth-no-bind-dn-search';
}
$message = new Message($msgkey, [
'dn' => "{$dn}#{$req->domain}",
]);
$message = $message->text();
$this->logger->info($message);
$this->logger->debug($e->getMessage());
}
}
}
/**
* #param $server
* #param $encryption
* #return mixed
*/
public function createLDAPObject($server, $encryption)
{
return Ldap::create('ext_ldap', [
'host' => $server,
'encryption' => $encryption
]);
}
Then, you can mock the member method createLDAPObject instead of mocking the static method Ldap::create, which should be easier.
However, I would recommend that you refactor your code, so that it is more readable and testable.
1- First of all, call_user_func_array() is not really test-friendly, and I think your requirements here are not too dynamic so you can replace that line with $ldap->bind($bind_with[0],$bind_with[1]);
2- Your connect method is too large to be tested. Please read about Code Smells - Long Methods
3- The method can be refactored into smaller version by decoupling the presentation from the logic. Like for example you are getting the Message object to get the text from the $msgkey just to log it, which is not helping in code readability and test-ability.
These are my first thoughts on the thing :)
Happy coding & testing :)

Related

Mocking PDO functions issue in phpunit

This is my function for which I am trying to test if it throws an exception or not:
public function myFunction(): bool
{
$dateInterval = new \DateTime();
$dateInterval->sub(new \DateInterval('PT24H'));
/** #var \PDOStatement $stmt */
$stmt = $this->pdo->prepare('SELECT SUM(`values`) FROM `event_tracker` WHERE `identifier` = ? AND `mutated_at` >= ? AND `source` = ? AND `customer` = ?');
$stmt->execute([$this->identifier, $dateInterval->getTimestamp(), $this->source, $this->customer]);
$sum = $stmt->fetchColumn(0);
if ($this->checkSourceType($this->source) && $sum >= $this->amountLimitCMS[$this->storeId] ){
$this->exceptionMessage($this->amountLimitCMS[$this->countryIso], $dateInterval->format('H:i d-m-Y'), $this->source);
}
if (!$this->checkSourceType($this->source) && $sum >= $this->amountLimitMagento[$this->storeId] ){
$this->exceptionMessage($this->amountLimitMagento[$this->storeId], $dateInterval->format('H:i d-m-Y'), $this->source);
}
return true;
}
This is my unitTest function:
public function testAmountCheckForCMS()
{
$query = [
'store_id' => 13,
'shipping_countryiso2' => 'DK',
'amount' => 4000,
];
$customer = '000003090';
$source = 'fulfillment_prod';
$container = new Container();
$container['db'] = function ($container) {
return $this->createMock(\PDO::class);
};
$dateInterval = new \DateTime();
$dateInterval->sub(new \DateInterval('PT24H'));
$ordersPerCustomer = new AmountPerCustomer($container, $customer, $query, $source);
$fetchAllMock = $this
->getMockBuilder('PDOStatement')
->setMethods(['execute'])
->getMock();
$fetchAllMock
->expects($this->once())->method('fetchColumn')
->will($this->returnValue($query['amount']));
try {
$ordersPerCustomer->assertPassedCriteria();
$this->fail("Expected Exception has not been raised.");
}catch (\Exception $error) {
$this->assertEquals($error->getMessage(), "Total order amount event given parameters exceed sum {$query['amount']} since {$dateInterval->format('H:i d-m-Y')} from source {$source}");
}
}
As you in see, in my function, which I would like to test, the execute and fetchColumn functions are used. How can I mock them ? Right now, when I run my tests I am getting this error message:
Trying to configure method "fetchColumn" which cannot be configured because it does not exist, has not been specified, is final, or is static
Any idea how can I fix this ? Thank you!
I know I am late here but you need to set that method in mockBuilder so that it can be mocked.
here is the sample code -
$this->getMockBuilder(PDOStatement::class)
->disableOriginalConstructor()
->setMethods(["fetchColumn"])
->getMock();

PHP/Laravel disappearing variable

I have some very strange behaviour in an app I am working on. In the example below there are 2 functions.
public function updatePhpVhostVersions(Request $r) {
$data = array(
'hostname' => $r['hostname'],
'username' => $r['username'],
'vhost' => $r['vhost'],
'php_version' => $r['php_version']
);
$result = Cpanel::setPhpVhostVersions($data)->data;
if($r->ajax()){
return Response::json($result);
}
return $result;
}
public function getInstalledPhpVersions(Request $r) {
$data = array(
'hostname' => $r['hostname'],
'username' => $r['username']
);
$result = Cpanel::getInstalledPhpVersions($data)->data;
if($r->ajax()){
return Response::json($result);
}
return $result;
}
The two functions contain Cpanel:: ... ($data)->data; this gets handeled by a __call method. In this method I use a function that prepares some vars, caching, etc. to simplify things I have combined some functions into one.
private function prepare($function, $arguments) {
// Dettirmine what will be used
$this->function = $function;
$nameSplit = preg_split('/(?=\p{Lu})/u', $function);
$this->class = 'App\\Phase\\Cpanel\\' . $this->folder($nameSplit) . '\\' . $this->file($nameSplit);
// Cache True/False
if(isset($this->arguments['cache'])) {
$this->cache = $this->arguments['cache'];
}
// Get the provided arguments
// these are used in the API post
if(isset($arguments[0]['hostname'], $arguments[0]['username'])) {
$arguments = $arguments[0];
}
$this->arguments = $arguments;
// Flush the cache when needed
if (!$this->cache || in_array($nameSplit[0], array('create', 'delete', 'add', 'install', 'set'))) {
try {
Cache::tags(['cpanel', $this->function . $arguments['username']])
->flush();
} catch (Exception $e) {
dd($arguments);
}
}
}
The prepare method is used every time the __call method gets used. In Cpanel::setPhpVhostVersion() the $arguments somehow get empty after the following if statement.
// Flush the cache when needed
if (!$this->cache || in_array($nameSplit[0], array('create', 'delete', 'add', 'install', 'set'))) {
try {
Cache::tags(['cpanel', $this->function . $arguments['username']])
->flush();
} catch (Exception $e) {
dd($e);
}
}
Before Cache::tags() the $arguments contains an array with some user information. But when Cache::tags()->flush() gets called it throws an exception that $arguments['username'] is empty. Now if I dd($arguments) after this, it returns an empty array. If I dd() before this the array still has the information. This only happens with Cpanel::setPhpVhostVersion() not with the 37 other possible Cpanel:: ... () what could be causing this?
EDIT
After some playing around with the code, I noticed that $arguments gets empty everytime after it gets used. It does not matter where, it just gets empty. (But only with setPhpVhostVersions)
Example
if(!isset($this->arguments['username'])) {
return Api::respondUnauthenticated('Account username is a required parameter');
}
Before the !isset() the $arguments['username'] exists, during and after the !isset() I get the exception:
ErrorException: Undefined index: username

Validating POST data class

This is an extension of a question I asked earlier that was deemed to be unsafe practise, due to the use of eval(). So I went for another approach but I have run into a problem. I do not know how to convert it to a class. My attempt ends with an error when I try to use call_user_func_array. it can't find the function in the class. Can you give me some hint so I get going? Thanks!
The error message I get when I try to run my code is Warning: call_user_func_array() expects parameter 1 to be a valid callback, function 'testlength' not found or invalid function name but on all validation methods. This is what I don't understand. This is what I want help to understand why it does not work.
class ruleValidator
{
protected $postData = array();
protected $ruleSet = array();
var $exceptions = 'Å,Ä,Þ,å,ä,þ,Ø,Ö,Ð,ø,ö,ð,Æ,Ü,æ,ü,á,é,í,ñ,ó,ú,ü,Á,É,Í,Ñ,Ó,Ê,Ú,Ü,ß';
function __construct(){
$this->exceptions = explode(',',$exceptions);
}
function testlength($string,$threshold)
{
return strlen($string)<$threshold?
'Your %s is too short.': // TRUE
''; // FALSE
}
function testnumeric($string,$offset,$length,$switch=true)
{
if(is_numeric(substr($string,$offset,$length))===$switch)
{
return $switch?
'Your %s has to begin with a character.': // Truely TRUE
'Your %s is containing non numeric characters. Please enter only digits.'; // Falsely TRUE
}
}
function testemail($string)
{
return filter_var($string, FILTER_VALIDATE_EMAIL)?
'': // TRUE
'Your email is not in a valid form.'; // FALSE
}
function testpattern($string,$pattern='/^[0-9]{8,10}$/')
{
return preg_match($pattern, $string)?
'': // TRUE
'Your %s is entered incorrect. Please use the correct format when entering data.'; // FALSE
}
function testequalto($string1,$string2)
{
return $string1==$string2?
'': // TRUE
'Your %s fields do not match eachother.'; // FALSE
}
function testchecked($bool)
{
return $bool===true?
'': // TRUE
'You are required to check this %s to continue.'; // FALSE
}
function testspecchar($string,$excludes=array())
{
if(is_array($excludes)&&!empty($excludes))
{
foreach($excludes as $exclude)
{
$string=str_replace($exclude,'',$string);
}
}
if(preg_match('/[^a-z0-9 ]+/i',$string))
{
return 'Your %s contains illegal characters.'; // TRUE
}
return; // FALSE
}
}
This is an array with how the POST data is recieved in the validator and the rules I use for the different fields in the form.
$exceptions = explode(',','Å,Ä,Þ,å,ä,þ,Ø,Ö,Ð,ø,ö,ð,Æ,Ü,æ,ü,á,é,í,ñ,ó,ú,ü,Á,É,Í,Ñ,Ó,Ê,Ú,Ü,ß');
$postData = array
(
'name' => 'Mikael',
'familyname' => 'Eriksson`',
'username' => 'Mik',
'password' => 'testtest',
'password-confirm' => 'testtesty',
'email' => 'try.to#guess.it,se',
'phone' => '0000000000a',
'policy' => 0
);
$ruleSet = array
(
'name'=>array
(
'testlength'=>2,
'testnumeric'=>array(0,1),
'testspecchar'=>array($exceptions)
),
'familyname'=>array
(
'testlength'=>2,
'testnumeric'=>array(0,1),
'testspecchar'=>array($exceptions)
),
'username'=>array
(
'testlength'=>4,
'testnumeric'=>array(0,1),
'testspecchar'=>array()
),
'email'=>array
(
'testemail'=>array()
),
'phone'=>array
(
'testnumeric'=>array(0,strlen($postData['phone']),false),
'testpattern'=>'/^[0-9]{8,10}$/'
),
'password'=>array
(
'testlength'=>8
),
'password-confirm'=>array
(
'testequalto'=>$postData['password-confirm']
),
'policy'=>array
(
'testchecked'=>array()
)
);
Here is how I validated the data up until now. It works, but I want to make this to a class to streamline the code in my project.
foreach($postData as $key => $value)
{
if(!array_key_exists($key,$ruleSet))
{
$errors[] = "The field `$key` is not part of the form. Only send actual form data.";
break;
}
$slice = array($key=>$ruleSet[$key]);
foreach($slice as $rules => $rule)
{
foreach($rule as $rls => $r)
{
$r = array_merge((array)$value,(array)$r);
$errors[] = sprintf(call_user_func_array($rls,$r),$key);
}
}
}
if(count($errors)>0) return implode(';;',array_filter($errors,'strlen'));
When you want to call a method of a class, you have to make an instance (using new) or call them statically when the methods are declared static.
In both ways, you have to tell call_user_func_array() that you are not calling a function in the global scope, but from within a class.
call_user_func_array(array('ruleValidator', $rls), $r)
Then declare the functions static:
public static function testlength($string,$threshold) {
}
Or with new:
$slice = array($key=>$ruleSet[$key]);
$callbackClass = new ruleValidator();
foreach($slice as $rules => $rule)
/** ... */
call_user_func_array(array($callbackClass, $rls), $r)
Thanks #Deadooshka for providing me with the solution.
call_user_func_array("ruleValidator::$rls", $r)

Laravel Inheritance Fail

I'm using Laravel 4 and I have this code here:
http://demo.php-pastebin.com/2sfuOUE7
Above the first line there is a line where I include another class file (CHPPConnection, which is a library for easier implement of OAuth 1.0, located at http://pht.htloto.org)
This is the code for the retrieveAccessToken method in that library:
/**
* Get access token for chpp application
*
* #param String $oauthToken
* #param String $verifier
*/
public function retrieveAccessToken($oauthToken, $verifier)
{
$params = array(
'oauth_consumer_key' => $this->consumerKey,
'oauth_signature_method' => $this->signatureMethod,
'oauth_timestamp' => $this->getTimestamp(),
'oauth_nonce' => $this->getNonce(),
'oauth_token' => $oauthToken,
'oauth_verifier' => $verifier,
'oauth_version' => $this->version
);
$signature = $this->buildSignature(self::OAUTH_SERVER.self::ACCESS_URL, $params, $this->oauthFirstTokenSecret);
$params['oauth_signature'] = $signature;
uksort($params, 'strcmp');
$url = $this->buildOauthUrl(self::OAUTH_SERVER.self::ACCESS_URL, $params);
if($this->canLog())
{
$this->log("[OAUTH] Access url: ".$url);
}
$return = $this->fetchUrl($url, false);
$result = explode('&', $return);
foreach($result as $val)
{
$t = explode('=', $val);
$$t[0] = urldecode($t[1]);
}
if(isset($oauth_token))
{
$this->setOauthToken($oauth_token);
if($this->canLog())
{
$this->log("[OAUTH] Access token: ".$oauth_token);
}
}
if(isset($oauth_token_secret))
{
$this->setOauthTokenSecret($oauth_token_secret);
if($this->canLog())
{
$this->log("[OAUTH] Access token secret: ".$oauth_token_secret);
}
}
}
Why is my code not working? Why the __constructor method returns results I want, but the something method doesn't? I probably have some wrong understanding how inheritance works in this case, so please help me out!
I think it's maybe because you are trying to return something in your constructor, so maybe when you instantiate it, you aren't retrieving an instance of it but an instance of pht, which obviously wouldn't have the something() function you are looking for.
class PhtController extends BaseController {
protected $_pht;
public function __construct()
{
$this->_pht = new CHPPConnection(
Config::get("pht.consumerKey"),
Config::get("pht.consumerSecret"),
Config::get("pht.callback"));
//this returns true
}
public function something()
{
$at = $this->_pht->retrieveAccessToken($_REQUEST["oauth_token"], $_REQUEST["oauth_verifier"]);
//vardump $at here dumps just NULL and cannot use any other methods aswell, returns false
}
}
// If you need to retrieve the instance of pht for any reason, call this function rather than returning it in the constructor.
public function getPHT()
{
return $this->_pht;
}

Predis Alias Sharding

I'm trying to use Predis sharding by alias, as described here. My code is basically identical, but I'm only returning empty arrays. Do my hash keys need {} around them? (EDIT: Nope, just tried it)
$api->get("/test", function () {
$servers = [
["alias" => "metadata", "port" => 6380],
["alias" => "relations", "port" => 6381],
["alias" => "dim_provider", "port" => 6382],
["alias" => "dim_revctrcode", "port" => 6383],
["alias" => "dim_enccode", "port" => 6384],
["alias" => "dim_pos", "port" => 6385]
];
$options = [
"nodehash" => function ($connection) { return $connection->getParameters()->alias; },
"cluster" => function ($options) {
$replicas = Predis\Cluster\Distribution\HashRing::DEFAULT_REPLICAS;
$hashring = new Predis\Cluster\Distribution\HashRing($replicas, $options->nodehash);
$cluster = new Predis\Connection\PredisCluster($hashring);
return $cluster;
}
];
$redis = new Predis\Client($servers, $options);
try {
$test = $redis->scard("dim_provider");
print_r($test); // Prints 0 for scard or empty Array for hgetall
} catch (Exception $e) {
print $e->getMessage();
}
$redis = new Predis\Client(["port" => 6382]);
$test = $redis->scard("dim_provider");
print_r($test); // Works.
});
EDIT: It also works if I only put one server in the $servers array. So it seems the hashing is not working right. When I throw some echos in front of the return value in nodehash I can see that it's returning the alias.
Assigning a dim_provider alias to a Redis connection and trying to get a key named dim_provider from a server are two different things.
In your script you are trying to set up a cluster of Redis instances using connection aliases (instead of the usual ip:port pairs) to calculate the distribution of your keyspace among multiple Redis servers acting as your data shards. Using this setup, the key dim_provider is sharded accordingly to the underlying distribution algorithm and could be stored on any of the 6 servers composing your cluster and defined in the $servers array.
I wanted to add how trivially easy it was to implement my clustering strategy once nrk got me on the right track. This is a really well-written library.
$api->get("/test", function () {
Class KeyCluster extends Predis\Connection\PredisCluster {
public function __construct() {
$this->pool = Array();
}
public function add (Predis\Connection\SingleConnectionInterface $connection) {
$parameters = $connection->getParameters();
if (isset($parameters->table)) {
$this->pool[$parameters->table] = $connection;
} else {
$this->pool[] = $connection;
}
}
public function getConnection (Command\CommandInterface $command) {
$key = $command->getArgument(0);
$table = explode(":", $key)[0];
return isset($this->pool[$table]) ? $this->pool[$table] : null;
}
}
$redis = new Predis\Client([
"tcp://127.0.0.1:6382?table=dim_provider",
"tcp://127.0.0.1:6383?table=dim_pos"
],[
"cluster" => new KeyCluster
]);
$result = $redis->scard("dim_provider");
print_r($result);
});

Categories