This commit is contained in:
2020-10-06 14:27:47 +07:00
commit 586be80cf6
16613 changed files with 3274099 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
<?php
namespace Codeception\Module;
use Codeception\Exception\ModuleException as ModuleException;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
use Exception;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exception\AMQPProtocolChannelException;
use PhpAmqpLib\Message\AMQPMessage;
/**
* This module interacts with message broker software that implements
* the Advanced Message Queuing Protocol (AMQP) standard. For example, RabbitMQ (tested).
*
* <div class="alert alert-info">
* To use this module with Composer you need <em>"php-amqplib/php-amqplib": "~2.4"</em> package.
* </div>
*
* ## Config
*
* * host: localhost - host to connect
* * username: guest - username to connect
* * password: guest - password to connect
* * vhost: '/' - vhost to connect
* * cleanup: true - defined queues will be purged before running every test.
* * queues: [mail, twitter] - queues to cleanup
* * single_channel - create and use only one channel during test execution
*
* ### Example
*
* modules:
* enabled:
* - AMQP:
* host: 'localhost'
* port: '5672'
* username: 'guest'
* password: 'guest'
* vhost: '/'
* queues: [queue1, queue2]
* single_channel: false
*
* ## Public Properties
*
* * connection - AMQPStreamConnection - current connection
*/
class AMQP extends CodeceptionModule implements RequiresPackage
{
protected $config = [
'host' => 'localhost',
'username' => 'guest',
'password' => 'guest',
'port' => '5672',
'vhost' => '/',
'cleanup' => true,
'single_channel' => false
];
/**
* @var AMQPStreamConnection
*/
public $connection;
/**
* @var int
*/
protected $channelId;
protected $requiredFields = ['host', 'username', 'password', 'vhost'];
public function _requires()
{
return ['PhpAmqpLib\Connection\AMQPStreamConnection' => '"php-amqplib/php-amqplib": "~2.4"'];
}
public function _initialize()
{
$host = $this->config['host'];
$port = $this->config['port'];
$username = $this->config['username'];
$password = $this->config['password'];
$vhost = $this->config['vhost'];
try {
$this->connection = new AMQPStreamConnection($host, $port, $username, $password, $vhost);
} catch (Exception $e) {
throw new ModuleException(__CLASS__, $e->getMessage() . ' while establishing connection to MQ server');
}
}
public function _before(TestInterface $test)
{
if ($this->config['cleanup']) {
$this->cleanup();
}
}
/**
* Sends message to exchange by sending exchange name, message
* and (optionally) a routing key
*
* ``` php
* <?php
* $I->pushToExchange('exchange.emails', 'thanks');
* $I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!'));
* $I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!'), 'severity');
* ?>
* ```
*
* @param string $exchange
* @param string|\PhpAmqpLib\Message\AMQPMessage $message
* @param string $routing_key
*/
public function pushToExchange($exchange, $message, $routing_key = null)
{
$message = $message instanceof AMQPMessage
? $message
: new AMQPMessage($message);
$this->getChannel()->basic_publish($message, $exchange, $routing_key);
}
/**
* Sends message to queue
*
* ``` php
* <?php
* $I->pushToQueue('queue.jobs', 'create user');
* $I->pushToQueue('queue.jobs', new AMQPMessage('create'));
* ?>
* ```
*
* @param string $queue
* @param string|\PhpAmqpLib\Message\AMQPMessage $message
*/
public function pushToQueue($queue, $message)
{
$message = $message instanceof AMQPMessage
? $message
: new AMQPMessage($message);
$this->getChannel()->queue_declare($queue);
$this->getChannel()->basic_publish($message, '', $queue);
}
/**
* Declares an exchange
*
* This is an alias of method `exchange_declare` of `PhpAmqpLib\Channel\AMQPChannel`.
*
* ```php
* <?php
* $I->declareExchange(
* 'nameOfMyExchange', // exchange name
* 'topic' // exchange type
* )
* ```
*
* @param string $exchange
* @param string $type
* @param bool $passive
* @param bool $durable
* @param bool $auto_delete
* @param bool $internal
* @param bool $nowait
* @param array $arguments
* @param int $ticket
* @return mixed|null
*/
public function declareExchange(
$exchange,
$type,
$passive = false,
$durable = false,
$auto_delete = true,
$internal = false,
$nowait = false,
$arguments = null,
$ticket = null
) {
return $this->getChannel()->exchange_declare(
$exchange,
$type,
$passive,
$durable,
$auto_delete,
$internal,
$nowait,
$arguments,
$ticket
);
}
/**
* Declares queue, creates if needed
*
* This is an alias of method `queue_declare` of `PhpAmqpLib\Channel\AMQPChannel`.
*
* ```php
* <?php
* $I->declareQueue(
* 'nameOfMyQueue', // exchange name
* )
* ```
*
* @param string $queue
* @param bool $passive
* @param bool $durable
* @param bool $exclusive
* @param bool $auto_delete
* @param bool $nowait
* @param array $arguments
* @param int $ticket
* @return mixed|null
*/
public function declareQueue(
$queue = '',
$passive = false,
$durable = false,
$exclusive = false,
$auto_delete = true,
$nowait = false,
$arguments = null,
$ticket = null
) {
return $this->getChannel()->queue_declare(
$queue,
$passive,
$durable,
$exclusive,
$auto_delete,
$nowait,
$arguments,
$ticket
);
}
/**
* Binds a queue to an exchange
*
* This is an alias of method `queue_bind` of `PhpAmqpLib\Channel\AMQPChannel`.
*
* ```php
* <?php
* $I->bindQueueToExchange(
* 'nameOfMyQueueToBind', // name of the queue
* 'transactionTracking.transaction', // exchange name to bind to
* 'your.routing.key' // Optionally, provide a binding key
* )
* ```
*
* @param string $queue
* @param string $exchange
* @param string $routing_key
* @param bool $nowait
* @param array $arguments
* @param int $ticket
* @return mixed|null
*/
public function bindQueueToExchange(
$queue,
$exchange,
$routing_key = '',
$nowait = false,
$arguments = null,
$ticket = null
) {
return $this->getChannel()->queue_bind(
$queue,
$exchange,
$routing_key,
$nowait,
$arguments,
$ticket
);
}
/**
* Checks if message containing text received.
*
* **This method drops message from queue**
* **This method will wait for message. If none is sent the script will stuck**.
*
* ``` php
* <?php
* $I->pushToQueue('queue.emails', 'Hello, davert');
* $I->seeMessageInQueueContainsText('queue.emails','davert');
* ?>
* ```
*
* @param string $queue
* @param string $text
*/
public function seeMessageInQueueContainsText($queue, $text)
{
$msg = $this->getChannel()->basic_get($queue);
if (!$msg) {
$this->fail("Message was not received");
}
if (!$msg instanceof AMQPMessage) {
$this->fail("Received message is not format of AMQPMessage");
}
$this->debugSection("Message", $msg->body);
$this->assertContains($text, $msg->body);
}
/**
* Takes last message from queue.
*
* ``` php
* <?php
* $message = $I->grabMessageFromQueue('queue.emails');
* ?>
* ```
*
* @param string $queue
* @return \PhpAmqpLib\Message\AMQPMessage
*/
public function grabMessageFromQueue($queue)
{
$message = $this->getChannel()->basic_get($queue);
return $message;
}
/**
* Purge a specific queue defined in config.
*
* ``` php
* <?php
* $I->purgeQueue('queue.emails');
* ?>
* ```
*
* @param string $queueName
*/
public function purgeQueue($queueName = '')
{
if (! in_array($queueName, $this->config['queues'])) {
throw new ModuleException(__CLASS__, "'$queueName' doesn't exist in queues config list");
}
$this->getChannel()->queue_purge($queueName, true);
}
/**
* Purge all queues defined in config.
*
* ``` php
* <?php
* $I->purgeAllQueues();
* ?>
* ```
*/
public function purgeAllQueues()
{
$this->cleanup();
}
/**
* @return \PhpAmqpLib\Channel\AMQPChannel
*/
protected function getChannel()
{
if ($this->config['single_channel'] && $this->channelId === null) {
$this->channelId = $this->connection->get_free_channel_id();
}
return $this->connection->channel($this->channelId);
}
protected function cleanup()
{
if (!isset($this->config['queues'])) {
throw new ModuleException(__CLASS__, "please set queues for cleanup");
}
if (!$this->connection) {
return;
}
foreach ($this->config['queues'] as $queue) {
try {
$this->getChannel()->queue_purge($queue);
} catch (AMQPProtocolChannelException $e) {
// ignore if exchange/queue doesn't exist and rethrow exception if it's something else
if ($e->getCode() !== 404) {
throw $e;
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Codeception\Module;
use Codeception\Step;
use Codeception\TestInterface;
use Facebook\WebDriver\WebDriverBy;
/**
* Module for AngularJS testing, based on [WebDriver module](http://codeception.com/docs/modules/WebDriver) and [Protractor](http://angular.github.io/protractor/).
*
* Performs **synchronization to ensure that page content is fully rendered**.
* Uses Angular's and Protractor internals methods to synchronize with the page.
*
* ## Configuration
*
* The same as for [WebDriver](http://codeception.com/docs/modules/WebDriver#Configuration), but few new options added:
*
* * `el` - element where Angular application is defined (default: `body`)
* * `script_timeout` - for how long in seconds to wait for angular operations to finish (default: 5)
*
* ### Example (`acceptance.suite.yml`)
*
* modules:
* enabled:
* - AngularJS:
* url: 'http://localhost/'
* browser: firefox
* script_timeout: 10
*
*
* ### Additional Features
*
* Can perform matching elements by model. In this case you should provide a strict locator with `model` set.
*
* Example:
*
* ```php
* $I->selectOption(['model' => 'customerId'], '3');
* ```
*/
class AngularJS extends WebDriver
{
protected $insideApplication = true;
protected $defaultAngularConfig = [
'script_timeout' => 5,
'el' => 'body',
];
protected $waitForAngular = <<<EOF
var rootSelector = arguments[0];
var callback = arguments[1];
var el = document.querySelector(rootSelector);
try {
if (window.getAngularTestability) {
window.getAngularTestability(el).whenStable(callback);
return;
}
if (!window.angular) {
throw new Error('window.angular is undefined. This could be either ' +
'because this is a non-angular page or because your test involves ' +
'client-side navigation, which can interfere with Protractor\'s ' +
'bootstrapping. See http://git.io/v4gXM for details');
}
if (angular.getTestability) {
angular.getTestability(el).whenStable(callback);
} else {
if (!angular.element(el).injector()) {
throw new Error('root element (' + rootSelector + ') has no injector.' +
' this may mean it is not inside ng-app.');
}
angular.element(el).injector().get('\$browser').
notifyWhenNoOutstandingRequests(callback);
}
} catch (err) {
callback(err.message);
}
EOF;
public function _setConfig($config)
{
parent::_setConfig(array_merge($this->defaultAngularConfig, $config));
}
public function _before(TestInterface $test)
{
parent::_before($test);
$this->webDriver->manage()->timeouts()->setScriptTimeout($this->config['script_timeout']);
}
/**
* Enables Angular mode (enabled by default).
* Waits for Angular to finish rendering after each action.
*/
public function amInsideAngularApp()
{
$this->insideApplication = true;
}
/**
* Disabled Angular mode.
*
* Falls back to original WebDriver, in case web page does not contain Angular app.
*/
public function amOutsideAngularApp()
{
$this->insideApplication = false;
}
public function _afterStep(Step $step)
{
if (!$this->insideApplication) {
return;
}
$actions = [
'amOnPage',
'click',
'fillField',
'selectOption',
'checkOption',
'uncheckOption',
'unselectOption',
'doubleClick',
'appendField',
'clickWithRightButton',
'dragAndDrop'
];
if (in_array($step->getAction(), $actions)) {
$this->webDriver->executeAsyncScript($this->waitForAngular, [$this->config['el']]);
}
}
protected function getStrictLocator(array $by)
{
$type = key($by);
$value = $by[$type];
if ($type === 'model') {
return WebDriverBy::cssSelector(sprintf('[ng-model="%s"]', $value));
}
return parent::getStrictLocator($by);
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Codeception\Module;
use Codeception\Module;
use Codeception\TestInterface;
use Codeception\Exception\ModuleException;
/**
* This module interacts with the [Alternative PHP Cache (APC)](http://php.net/manual/en/intro.apcu.php)
* using either _APCu_ or _APC_ extension.
*
* Performs a cleanup by flushing all values after each test run.
*
* ## Status
*
* * Maintainer: **Serghei Iakovlev**
* * Stability: **stable**
* * Contact: serghei@phalconphp.com
*
* ### Example (`unit.suite.yml`)
*
* ```yaml
* modules:
* - Apc
* ```
*
* Be sure you don't use the production server to connect.
*
*/
class Apc extends Module
{
/**
* Code to run before each test.
*
* @param TestInterface $test
* @throws ModuleException
*/
public function _before(TestInterface $test)
{
if (!extension_loaded('apc') && !extension_loaded('apcu')) {
throw new ModuleException(
__CLASS__,
'The APC(u) extension not loaded.'
);
}
if (!ini_get('apc.enabled') || (PHP_SAPI === 'cli' && !ini_get('apc.enable_cli'))) {
throw new ModuleException(
__CLASS__,
'The "apc.enable_cli" parameter must be set to "On".'
);
}
}
/**
* Code to run after each test.
*
* @param TestInterface $test
*/
public function _after(TestInterface $test)
{
$this->clear();
}
/**
* Grabs value from APC(u) by key.
*
* Example:
*
* ``` php
* <?php
* $users_count = $I->grabValueFromApc('users_count');
* ?>
* ```
*
* @param string|string[] $key
* @return mixed
*/
public function grabValueFromApc($key)
{
$value = $this->fetch($key);
$this->debugSection('Value', $value);
return $value;
}
/**
* Checks item in APC(u) exists and the same as expected.
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key exists
* $I->seeInApc('users_count');
*
* // Checks a 'users_count' exists and has the value 200
* $I->seeInApc('users_count', 200);
* ?>
* ```
*
* @param string|string[] $key
* @param mixed $value
*/
public function seeInApc($key, $value = null)
{
if (null === $value) {
$this->assertTrue($this->exists($key), "Cannot find key '$key' in APC(u).");
return;
}
$actual = $this->grabValueFromApc($key);
$this->assertEquals($value, $actual, "Cannot find key '$key' in APC(u) with the provided value.");
}
/**
* Checks item in APC(u) doesn't exist or is the same as expected.
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key does not exist
* $I->dontSeeInApc('users_count');
*
* // Checks a 'users_count' exists does not exist or its value is not the one provided
* $I->dontSeeInApc('users_count', 200);
* ?>
* ```
*
* @param string|string[] $key
* @param mixed $value
*/
public function dontSeeInApc($key, $value = null)
{
if (null === $value) {
$this->assertFalse($this->exists($key), "The key '$key' exists in APC(u).");
return;
}
$actual = $this->grabValueFromApc($key);
if (false !== $actual) {
$this->assertEquals($value, $actual, "The key '$key' exists in APC(u) with the provided value.");
}
}
/**
* Stores an item `$value` with `$key` on the APC(u).
*
* Examples:
*
* ```php
* <?php
* // Array
* $I->haveInApc('users', ['name' => 'miles', 'email' => 'miles@davis.com']);
*
* // Object
* $I->haveInApc('user', UserRepository::findFirst());
*
* // Key as array of 'key => value'
* $entries = [];
* $entries['key1'] = 'value1';
* $entries['key2'] = 'value2';
* $entries['key3'] = ['value3a','value3b'];
* $entries['key4'] = 4;
* $I->haveInApc($entries, null);
* ?>
* ```
*
* @param string|array $key
* @param mixed $value
* @param int $expiration
* @return mixed
*/
public function haveInApc($key, $value, $expiration = null)
{
$this->store($key, $value, $expiration);
return $key;
}
/**
* Clears the APC(u) cache
*/
public function flushApc()
{
// Returns TRUE always
$this->clear();
}
/**
* Clears the APC(u) cache.
*
* @return bool
*/
protected function clear()
{
if (function_exists('apcu_clear_cache')) {
return apcu_clear_cache();
}
return apc_clear_cache('user');
}
/**
* Checks if entry exists
*
* @param string|string[] $key
*
* @return bool|\string[]
*/
protected function exists($key)
{
if (function_exists('apcu_exists')) {
return apcu_exists($key);
}
return apc_exists($key);
}
/**
* Fetch a stored variable from the cache
*
* @param string|string[] $key
*
* @return mixed
*/
protected function fetch($key)
{
$success = false;
if (function_exists('apcu_fetch')) {
$data = apcu_fetch($key, $success);
} else {
$data = apc_fetch($key, $success);
}
$this->debugSection('Fetching a stored variable', $success ? 'OK' : 'FAILED');
return $data;
}
/**
* Cache a variable in the data store.
*
* @param string|array $key
* @param mixed $var
* @param int $ttl
*
* @return array|bool
*/
protected function store($key, $var, $ttl = 0)
{
if (function_exists('apcu_store')) {
return apcu_store($key, $var, $ttl);
}
return apc_store($key, $var, $ttl);
}
}

View File

@@ -0,0 +1,494 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
/**
* Special module for using asserts in your tests.
*/
class Asserts extends CodeceptionModule
{
/**
* Checks that two variables are equal. If you're comparing floating-point values,
* you can specify the optional "delta" parameter which dictates how great of a precision
* error are you willing to tolerate in order to consider the two values equal.
*
* Regular example:
* ```php
* <?php
* $I->assertEquals($element->getChildrenCount(), 5);
* ```
*
* Floating-point example:
* ```php
* <?php
* $I->assertEquals($calculator->add(0.1, 0.2), 0.3, 'Calculator should add the two numbers correctly.', 0.01);
* ```
*
* @param $expected
* @param $actual
* @param string $message
* @param float $delta
*/
public function assertEquals($expected, $actual, $message = '', $delta = 0.0)
{
parent::assertEquals($expected, $actual, $message, $delta);
}
/**
* Checks that two variables are not equal. If you're comparing floating-point values,
* you can specify the optional "delta" parameter which dictates how great of a precision
* error are you willing to tolerate in order to consider the two values not equal.
*
* Regular example:
* ```php
* <?php
* $I->assertNotEquals($element->getChildrenCount(), 0);
* ```
*
* Floating-point example:
* ```php
* <?php
* $I->assertNotEquals($calculator->add(0.1, 0.2), 0.4, 'Calculator should add the two numbers correctly.', 0.01);
* ```
*
* @param $expected
* @param $actual
* @param string $message
* @param float $delta
*/
public function assertNotEquals($expected, $actual, $message = '', $delta = 0.0)
{
parent::assertNotEquals($expected, $actual, $message, $delta);
}
/**
* Checks that two variables are same
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertSame($expected, $actual, $message = '')
{
parent::assertSame($expected, $actual, $message);
}
/**
* Checks that two variables are not same
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertNotSame($expected, $actual, $message = '')
{
parent::assertNotSame($expected, $actual, $message);
}
/**
* Checks that actual is greater than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertGreaterThan($expected, $actual, $message = '')
{
parent::assertGreaterThan($expected, $actual, $message);
}
/**
* Checks that actual is greater or equal than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertGreaterThanOrEqual($expected, $actual, $message = '')
{
parent::assertGreaterThanOrEqual($expected, $actual, $message);
}
/**
* Checks that actual is less than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertLessThan($expected, $actual, $message = '')
{
parent::assertLessThan($expected, $actual, $message);
}
/**
* Checks that actual is less or equal than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
public function assertLessThanOrEqual($expected, $actual, $message = '')
{
parent::assertLessThanOrEqual($expected, $actual, $message);
}
/**
* Checks that haystack contains needle
*
* @param $needle
* @param $haystack
* @param string $message
*/
public function assertContains($needle, $haystack, $message = '')
{
parent::assertContains($needle, $haystack, $message);
}
/**
* Checks that haystack doesn't contain needle.
*
* @param $needle
* @param $haystack
* @param string $message
*/
public function assertNotContains($needle, $haystack, $message = '')
{
parent::assertNotContains($needle, $haystack, $message);
}
/**
* Checks that string match with pattern
*
* @param string $pattern
* @param string $string
* @param string $message
*/
public function assertRegExp($pattern, $string, $message = '')
{
parent::assertRegExp($pattern, $string, $message);
}
/**
* Checks that string not match with pattern
*
* @param string $pattern
* @param string $string
* @param string $message
*/
public function assertNotRegExp($pattern, $string, $message = '')
{
parent::assertNotRegExp($pattern, $string, $message);
}
/**
* Checks that a string starts with the given prefix.
*
* @param string $prefix
* @param string $string
* @param string $message
*/
public function assertStringStartsWith($prefix, $string, $message = '')
{
parent::assertStringStartsWith($prefix, $string, $message);
}
/**
* Checks that a string doesn't start with the given prefix.
*
* @param string $prefix
* @param string $string
* @param string $message
*/
public function assertStringStartsNotWith($prefix, $string, $message = '')
{
parent::assertStringStartsNotWith($prefix, $string, $message);
}
/**
* Checks that variable is empty.
*
* @param $actual
* @param string $message
*/
public function assertEmpty($actual, $message = '')
{
parent::assertEmpty($actual, $message);
}
/**
* Checks that variable is not empty.
*
* @param $actual
* @param string $message
*/
public function assertNotEmpty($actual, $message = '')
{
parent::assertNotEmpty($actual, $message);
}
/**
* Checks that variable is NULL
*
* @param $actual
* @param string $message
*/
public function assertNull($actual, $message = '')
{
parent::assertNull($actual, $message);
}
/**
* Checks that variable is not NULL
*
* @param $actual
* @param string $message
*/
public function assertNotNull($actual, $message = '')
{
parent::assertNotNull($actual, $message);
}
/**
* Checks that condition is positive.
*
* @param $condition
* @param string $message
*/
public function assertTrue($condition, $message = '')
{
parent::assertTrue($condition, $message);
}
/**
* Checks that the condition is NOT true (everything but true)
*
* @param $condition
* @param string $message
*/
public function assertNotTrue($condition, $message = '')
{
parent::assertNotTrue($condition, $message);
}
/**
* Checks that condition is negative.
*
* @param $condition
* @param string $message
*/
public function assertFalse($condition, $message = '')
{
parent::assertFalse($condition, $message);
}
/**
* Checks that the condition is NOT false (everything but false)
*
* @param $condition
* @param string $message
*/
public function assertNotFalse($condition, $message = '')
{
parent::assertNotFalse($condition, $message);
}
/**
* Checks if file exists
*
* @param string $filename
* @param string $message
*/
public function assertFileExists($filename, $message = '')
{
parent::assertFileExists($filename, $message);
}
/**
* Checks if file doesn't exist
*
* @param string $filename
* @param string $message
*/
public function assertFileNotExists($filename, $message = '')
{
parent::assertFileNotExists($filename, $message);
}
/**
* @param $expected
* @param $actual
* @param $description
*/
public function assertGreaterOrEquals($expected, $actual, $description = '')
{
$this->assertGreaterThanOrEqual($expected, $actual, $description);
}
/**
* @param $expected
* @param $actual
* @param $description
*/
public function assertLessOrEquals($expected, $actual, $description = '')
{
$this->assertLessThanOrEqual($expected, $actual, $description);
}
/**
* @param $actual
* @param $description
*/
public function assertIsEmpty($actual, $description = '')
{
$this->assertEmpty($actual, $description);
}
/**
* @param $key
* @param $actual
* @param $description
*/
public function assertArrayHasKey($key, $actual, $description = '')
{
parent::assertArrayHasKey($key, $actual, $description);
}
/**
* @param $key
* @param $actual
* @param $description
*/
public function assertArrayNotHasKey($key, $actual, $description = '')
{
parent::assertArrayNotHasKey($key, $actual, $description);
}
/**
* Checks that array contains subset.
*
* @param array $subset
* @param array $array
* @param bool $strict
* @param string $message
*/
public function assertArraySubset($subset, $array, $strict = false, $message = '')
{
parent::assertArraySubset($subset, $array, $strict, $message);
}
/**
* @param $expectedCount
* @param $actual
* @param $description
*/
public function assertCount($expectedCount, $actual, $description = '')
{
parent::assertCount($expectedCount, $actual, $description);
}
/**
* @param $class
* @param $actual
* @param $description
*/
public function assertInstanceOf($class, $actual, $description = '')
{
parent::assertInstanceOf($class, $actual, $description);
}
/**
* @param $class
* @param $actual
* @param $description
*/
public function assertNotInstanceOf($class, $actual, $description = '')
{
parent::assertNotInstanceOf($class, $actual, $description);
}
/**
* @param $type
* @param $actual
* @param $description
*/
public function assertInternalType($type, $actual, $description = '')
{
parent::assertInternalType($type, $actual, $description);
}
/**
* Fails the test with message.
*
* @param $message
*/
public function fail($message)
{
parent::fail($message);
}
/**
* Handles and checks exception called inside callback function.
* Either exception class name or exception instance should be provided.
*
* ```php
* <?php
* $I->expectException(MyException::class, function() {
* $this->doSomethingBad();
* });
*
* $I->expectException(new MyException(), function() {
* $this->doSomethingBad();
* });
* ```
* If you want to check message or exception code, you can pass them with exception instance:
* ```php
* <?php
* // will check that exception MyException is thrown with "Don't do bad things" message
* $I->expectException(new MyException("Don't do bad things"), function() {
* $this->doSomethingBad();
* });
* ```
*
* @param $exception string or \Exception
* @param $callback
*/
public function expectException($exception, $callback)
{
$code = null;
$msg = null;
if (is_object($exception)) {
/** @var $exception \Exception **/
$class = get_class($exception);
$msg = $exception->getMessage();
$code = $exception->getCode();
} else {
$class = $exception;
}
try {
$callback();
} catch (\Exception $e) {
if (!$e instanceof $class) {
$this->fail(sprintf("Exception of class $class expected to be thrown, but %s caught", get_class($e)));
}
if (null !== $msg and $e->getMessage() !== $msg) {
$this->fail(sprintf(
"Exception of $class expected to be '$msg', but actual message was '%s'",
$e->getMessage()
));
}
if (null !== $code and $e->getCode() !== $code) {
$this->fail(sprintf(
"Exception of $class expected to have code $code, but actual code was %s",
$e->getCode()
));
}
$this->assertTrue(true); // increment assertion counter
return;
}
$this->fail("Expected exception of $class to be thrown, but nothing was caught");
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
/**
* Wrapper for basic shell commands and shell output
*
* ## Responsibility
* * Maintainer: **davert**
* * Status: **stable**
* * Contact: codecept@davert.mail.ua
*
* *Please review the code of non-stable modules and provide patches if you have issues.*
*/
class Cli extends CodeceptionModule
{
public $output = '';
public $result = null;
public function _before(TestInterface $test)
{
$this->output = '';
}
/**
* Executes a shell command.
* Fails If exit code is > 0. You can disable this by setting second parameter to false
*
* ```php
* <?php
* $I->runShellCommand('phpunit');
*
* // do not fail test when command fails
* $I->runShellCommand('phpunit', false);
* ```
*
* @param $command
* @param bool $failNonZero
*/
public function runShellCommand($command, $failNonZero = true)
{
$data = [];
exec("$command", $data, $resultCode);
$this->result = $resultCode;
$this->output = implode("\n", $data);
if ($this->output === null) {
\PHPUnit\Framework\Assert::fail("$command can't be executed");
}
if ($resultCode !== 0 && $failNonZero) {
\PHPUnit\Framework\Assert::fail("Result code was $resultCode.\n\n" . $this->output);
}
$this->debug(preg_replace('~s/\e\[\d+(?>(;\d+)*)m//g~', '', $this->output));
}
/**
* Checks that output from last executed command contains text
*
* @param $text
*/
public function seeInShellOutput($text)
{
\PHPUnit\Framework\Assert::assertContains($text, $this->output);
}
/**
* Checks that output from latest command doesn't contain text
*
* @param $text
*
*/
public function dontSeeInShellOutput($text)
{
$this->debug($this->output);
\PHPUnit\Framework\Assert::assertNotContains($text, $this->output);
}
/**
* @param $regex
*/
public function seeShellOutputMatches($regex)
{
\PHPUnit\Framework\Assert::assertRegExp($regex, $this->output);
}
/**
* Checks result code
*
* ```php
* <?php
* $I->seeResultCodeIs(0);
* ```
*
* @param $code
*/
public function seeResultCodeIs($code)
{
$this->assertEquals($this->result, $code, "result code is $code");
}
/**
* Checks result code
*
* ```php
* <?php
* $I->seeResultCodeIsNot(0);
* ```
*
* @param $code
*/
public function seeResultCodeIsNot($code)
{
$this->assertNotEquals($this->result, $code, "result code is $code");
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\DataMapper;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Interfaces\ORM;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\TestInterface;
use League\FactoryMuffin\FactoryMuffin;
use League\FactoryMuffin\Stores\RepositoryStore;
/**
* DataFactory allows you to easily generate and create test data using [**FactoryMuffin**](https://github.com/thephpleague/factory-muffin).
* DataFactory uses an ORM of your application to define, save and cleanup data. Thus, should be used with ORM or Framework modules.
*
* This module requires packages installed:
*
* ```json
* {
* "league/factory-muffin": "^3.0",
* }
* ```
*
* Generation rules can be defined in a factories file. You will need to create `factories.php` (it is recommended to store it in `_support` dir)
* Follow [FactoryMuffin documentation](https://github.com/thephpleague/factory-muffin) to set valid rules.
* Random data provided by [Faker](https://github.com/fzaninotto/Faker) library.
*
* ```php
* <?php
* use League\FactoryMuffin\Faker\Facade as Faker;
*
* $fm->define(User::class)->setDefinitions([
* 'name' => Faker::name(),
*
* // generate email
* 'email' => Faker::email(),
* 'body' => Faker::text(),
*
* // generate a profile and return its Id
* 'profile_id' => 'factory|Profile'
* ]);
* ```
*
* Configure this module to load factory definitions from a directory.
* You should also specify a module with an ORM as a dependency.
*
* ```yaml
* modules:
* enabled:
* - Yii2:
* configFile: path/to/config.php
* - DataFactory:
* factories: tests/_support/factories
* depends: Yii2
* ```
*
* (you can also use Laravel5 and Phalcon).
*
* In this example factories are loaded from `tests/_support/factories` directory. Please note that this directory is relative from the codeception.yml file (so for Yii2 it would be codeception/_support/factories).
* You should create this directory manually and create PHP files in it with factories definitions following [official documentation](https://github.com/thephpleague/factory-muffin#usage).
*
* In cases you want to use data from database inside your factory definitions you can define them in Helper.
* For instance, if you use Doctrine, this allows you to access `EntityManager` inside a definition.
*
* To proceed you should create Factories helper via `generate:helper` command and enable it:
*
* ```
* modules:
* enabled:
* - DataFactory:
* depends: Doctrine2
* - \Helper\Factories
*
* ```
*
* In this case you can define factories from a Helper class with `_define` method.
*
* ```php
* <?php
* public function _beforeSuite()
* {
* $factory = $this->getModule('DataFactory');
* // let us get EntityManager from Doctrine
* $em = $this->getModule('Doctrine2')->_getEntityManager();
*
* $factory->_define(User::class, [
*
* // generate random user name
* // use League\FactoryMuffin\Faker\Facade as Faker;
* 'name' => Faker::name(),
*
* // get real company from database
* 'company' => $em->getRepository(Company::class)->find(),
*
* // let's generate a profile for each created user
* // receive an entity and set it via `setProfile` method
* // UserProfile factory should be defined as well
* 'profile' => 'entity|'.UserProfile::class
* ]);
* }
* ```
*
* Factory Definitions are described in official [Factory Muffin Documentation](https://github.com/thephpleague/factory-muffin)
*
* ### Related Models Generators
*
* If your module relies on other model you can generate them both.
* To create a related module you can use either `factory` or `entity` prefix, depending on ORM you use.
*
* In case your ORM expects an Id of a related record (Eloquent) to be set use `factory` prefix:
*
* ```php
* 'user_id' => 'factory|User'
* ```
*
* In case your ORM expects a related record itself (Doctrine) then you should use `entity` prefix:
*
* ```php
* 'user' => 'entity|User'
* ```
*/
class DataFactory extends \Codeception\Module implements DependsOnModule, RequiresPackage
{
protected $dependencyMessage = <<<EOF
ORM module (like Doctrine2) or Framework module with ActiveRecord support is required:
--
modules:
enabled:
- DataFactory:
depends: Doctrine2
--
EOF;
/**
* ORM module on which we we depend on.
*
* @var ORM
*/
public $ormModule;
/**
* @var FactoryMuffin
*/
public $factoryMuffin;
protected $config = ['factories' => null];
public function _requires()
{
return [
'League\FactoryMuffin\FactoryMuffin' => '"league/factory-muffin": "^3.0"',
];
}
public function _beforeSuite($settings = [])
{
$store = $this->getStore();
$this->factoryMuffin = new FactoryMuffin($store);
if ($this->config['factories']) {
foreach ((array) $this->config['factories'] as $factoryPath) {
$realpath = realpath(codecept_root_dir().$factoryPath);
if ($realpath === false) {
throw new ModuleException($this, 'The path to one of your factories is not correct. Please specify the directory relative to the codeception.yml file (ie. _support/factories).');
}
$this->factoryMuffin->loadFactories($realpath);
}
}
}
/**
* @return StoreInterface|null
*/
protected function getStore()
{
return $this->ormModule instanceof DataMapper
? new RepositoryStore($this->ormModule->_getEntityManager()) // for Doctrine
: null;
}
public function _inject(ORM $orm)
{
$this->ormModule = $orm;
}
public function _after(TestInterface $test)
{
$skipCleanup = array_key_exists('cleanup', $this->config) && $this->config['cleanup'] === false;
if ($skipCleanup || $this->ormModule->_getConfig('cleanup')) {
return;
}
$this->factoryMuffin->deleteSaved();
}
public function _depends()
{
return ['Codeception\Lib\Interfaces\ORM' => $this->dependencyMessage];
}
/**
* Creates a model definition. This can be used from a helper:.
*
* ```php
* $this->getModule('{{MODULE_NAME}}')->_define('User', [
* 'name' => $faker->name,
* 'email' => $faker->email
* ]);
*
* ```
*
* @param string $model
* @param array $fields
*
* @return \League\FactoryMuffin\Definition
*
* @throws \League\FactoryMuffin\Exceptions\DefinitionAlreadyDefinedException
*/
public function _define($model, $fields)
{
return $this->factoryMuffin->define($model)->setDefinitions($fields);
}
/**
* Generates and saves a record,.
*
* ```php
* $I->have('User'); // creates user
* $I->have('User', ['is_active' => true]); // creates active user
* ```
*
* Returns an instance of created user.
*
* @param string $name
* @param array $extraAttrs
*
* @return object
*/
public function have($name, array $extraAttrs = [])
{
return $this->factoryMuffin->create($name, $extraAttrs);
}
/**
* Generates a record instance.
*
* This does not save it in the database. Use `have` for that.
*
* ```php
* $user = $I->make('User'); // return User instance
* $activeUser = $I->make('User', ['is_active' => true]); // return active user instance
* ```
*
* Returns an instance of created user without creating a record in database.
*
* @param string $name
* @param array $extraAttrs
*
* @return object
*/
public function make($name, array $extraAttrs = [])
{
return $this->factoryMuffin->instance($name, $extraAttrs);
}
/**
* Generates and saves a record multiple times.
*
* ```php
* $I->haveMultiple('User', 10); // create 10 users
* $I->haveMultiple('User', 10, ['is_active' => true]); // create 10 active users
* ```
*
* @param string $name
* @param int $times
* @param array $extraAttrs
*
* @return \object[]
*/
public function haveMultiple($name, $times, array $extraAttrs = [])
{
return $this->factoryMuffin->seed($times, $name, $extraAttrs);
}
}

View File

@@ -0,0 +1,682 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Interfaces\Db as DbInterface;
use Codeception\Lib\Driver\Db as Driver;
use Codeception\Lib\DbPopulator;
use Codeception\TestInterface;
/**
* Access a database.
*
* The most important function of this module is to clean a database before each test.
* This module also provides actions to perform checks in a database, e.g. [seeInDatabase()](http://codeception.com/docs/modules/Db#seeInDatabase)
*
* In order to have your database populated with data you need a raw SQL dump.
* Simply put the dump in the `tests/_data` directory (by default) and specify the path in the config.
* The next time after the database is cleared, all your data will be restored from the dump.
* Don't forget to include `CREATE TABLE` statements in the dump.
*
* Supported and tested databases are:
*
* * MySQL
* * SQLite (i.e. just one file)
* * PostgreSQL
*
* Also available:
*
* * MS SQL
* * Oracle
*
* Connection is done by database Drivers, which are stored in the `Codeception\Lib\Driver` namespace.
* [Check out the drivers](https://github.com/Codeception/Codeception/tree/2.3/src/Codeception/Lib/Driver)
* if you run into problems loading dumps and cleaning databases.
*
* ## Config
*
* * dsn *required* - PDO DSN
* * user *required* - username to access database
* * password *required* - password
* * dump - path to database dump
* * populate: false - whether the the dump should be loaded before the test suite is started
* * cleanup: false - whether the dump should be reloaded before each test
* * reconnect: false - whether the module should reconnect to the database before each test
* * waitlock: 0 - wait lock (in seconds) that the database session should use for DDL statements
* * ssl_key - path to the SSL key (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-key)
* * ssl_cert - path to the SSL certificate (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-cert)
* * ssl_ca - path to the SSL certificate authority (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-ssl-ca)
* * ssl_verify_server_cert - disables certificate CN verification (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php)
* * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher)
*
* ## Example
*
* modules:
* enabled:
* - Db:
* dsn: 'mysql:host=localhost;dbname=testdb'
* user: 'root'
* password: ''
* dump: 'tests/_data/dump.sql'
* populate: true
* cleanup: true
* reconnect: true
* waitlock: 10
* ssl_key: '/path/to/client-key.pem'
* ssl_cert: '/path/to/client-cert.pem'
* ssl_ca: '/path/to/ca-cert.pem'
* ssl_verify_server_cert: false
* ssl_cipher: 'AES256-SHA'
*
* ## SQL data dump
*
* There are two ways of loading the dump into your database:
*
* ### Populator
*
* The recommended approach is to configure a `populator`, an external command to load a dump. Command parameters like host, username, password, database
* can be obtained from the config and inserted into placeholders:
*
* For MySQL:
*
* ```yaml
* modules:
* enabled:
* - Db:
* dsn: 'mysql:host=localhost;dbname=testdb'
* user: 'root'
* password: ''
* dump: 'tests/_data/dump.sql'
* populate: true # run populator before all tests
* cleanup: true # run populator before each test
* populator: 'mysql -u $user -h $host $dbname < $dump'
* ```
*
* For PostgreSQL (using pg_restore)
*
* ```
* modules:
* enabled:
* - Db:
* dsn: 'pgsql:host=localhost;dbname=testdb'
* user: 'root'
* password: ''
* dump: 'tests/_data/db_backup.dump'
* populate: true # run populator before all tests
* cleanup: true # run populator before each test
* populator: 'pg_restore -u $user -h $host -D $dbname < $dump'
* ```
*
* Variable names are being taken from config and DSN which has a `keyword=value` format, so you should expect to have a variable named as the
* keyword with the full value inside it.
*
* PDO dsn elements for the supported drivers:
* * MySQL: [PDO_MYSQL DSN](https://secure.php.net/manual/en/ref.pdo-mysql.connection.php)
* * SQLite: [PDO_SQLITE DSN](https://secure.php.net/manual/en/ref.pdo-sqlite.connection.php)
* * PostgreSQL: [PDO_PGSQL DSN](https://secure.php.net/manual/en/ref.pdo-pgsql.connection.php)
* * MSSQL: [PDO_SQLSRV DSN](https://secure.php.net/manual/en/ref.pdo-sqlsrv.connection.php)
* * Oracle: [PDO_OCI DSN](https://secure.php.net/manual/en/ref.pdo-oci.connection.php)
*
* ### Dump
*
* Db module by itself can load SQL dump without external tools by using current database connection.
* This approach is system-independent, however, it is slower than using a populator and may have parsing issues (see below).
*
* Provide a path to SQL file in `dump` config option:
*
* ```yaml
* modules:
* enabled:
* - Db:
* dsn: 'mysql:host=localhost;dbname=testdb'
* user: 'root'
* password: ''
* populate: true # load dump before all tests
* cleanup: true # load dump for each test
* dump: 'tests/_data/dump.sql'
* ```
*
* To parse SQL Db file, it should follow this specification:
* * Comments are permitted.
* * The `dump.sql` may contain multiline statements.
* * The delimiter, a semi-colon in this case, must be on the same line as the last statement:
*
* ```sql
* -- Add a few contacts to the table.
* REPLACE INTO `Contacts` (`created`, `modified`, `status`, `contact`, `first`, `last`) VALUES
* (NOW(), NOW(), 1, 'Bob Ross', 'Bob', 'Ross'),
* (NOW(), NOW(), 1, 'Fred Flintstone', 'Fred', 'Flintstone');
*
* -- Remove existing orders for testing.
* DELETE FROM `Order`;
* ```
* ## Query generation
*
* `seeInDatabase`, `dontSeeInDatabase`, `seeNumRecords`, `grabFromDatabase` and `grabNumRecords` methods
* accept arrays as criteria. WHERE condition is generated using item key as a field name and
* item value as a field value.
*
* Example:
* ```php
* <?php
* $I->seeInDatabase('users', array('name' => 'Davert', 'email' => 'davert@mail.com'));
*
* ```
* Will generate:
*
* ```sql
* SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` = 'davert@mail.com'
* ```
* Since version 2.1.9 it's possible to use LIKE in a condition, as shown here:
*
* ```php
* <?php
* $I->seeInDatabase('users', array('name' => 'Davert', 'email like' => 'davert%'));
*
* ```
* Will generate:
*
* ```sql
* SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` LIKE 'davert%'
* ```
* ## Public Properties
* * dbh - contains the PDO connection
* * driver - contains the Connection Driver
*
*/
class Db extends CodeceptionModule implements DbInterface
{
/**
* @api
* @var
*/
public $dbh;
/**
* @var array
*/
protected $sql = [];
/**
* @var array
*/
protected $config = [
'populate' => false,
'cleanup' => false,
'reconnect' => false,
'waitlock' => 0,
'dump' => null,
'populator' => null,
];
/**
* @var bool
*/
protected $populated = false;
/**
* @var \Codeception\Lib\Driver\Db
*/
public $driver;
/**
* @var array
*/
protected $insertedRows = [];
/**
* @var array
*/
protected $requiredFields = ['dsn', 'user', 'password'];
public function _initialize()
{
$this->connect();
}
public function __destruct()
{
$this->disconnect();
}
public function _beforeSuite($settings = [])
{
if (!$this->config['populator']
&& $this->config['dump']
&& ($this->config['cleanup'] || ($this->config['populate']))
) {
$this->readSql();
}
$this->connect();
// starting with loading dump
if ($this->config['populate']) {
if ($this->config['cleanup']) {
$this->_cleanup();
}
$this->_loadDump();
}
if ($this->config['reconnect']) {
$this->disconnect();
}
}
private function readSql()
{
if (!file_exists(Configuration::projectDir() . $this->config['dump'])) {
throw new ModuleConfigException(
__CLASS__,
"\nFile with dump doesn't exist.\n"
. "Please, check path for sql file: "
. $this->config['dump']
);
}
$sql = file_get_contents(Configuration::projectDir() . $this->config['dump']);
// remove C-style comments (except MySQL directives)
$sql = preg_replace('%/\*(?!!\d+).*?\*/%s', '', $sql);
if (!empty($sql)) {
// split SQL dump into lines
$this->sql = preg_split('/\r\n|\n|\r/', $sql, -1, PREG_SPLIT_NO_EMPTY);
}
}
private function connect()
{
$options = [];
/**
* @see http://php.net/manual/en/pdo.construct.php
* @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
*/
if (array_key_exists('ssl_key', $this->config)
&& !empty($this->config['ssl_key'])
&& defined('\PDO::MYSQL_ATTR_SSL_KEY')
) {
$options[\PDO::MYSQL_ATTR_SSL_KEY] = (string) $this->config['ssl_key'];
}
if (array_key_exists('ssl_cert', $this->config)
&& !empty($this->config['ssl_cert'])
&& defined('\PDO::MYSQL_ATTR_SSL_CERT')
) {
$options[\PDO::MYSQL_ATTR_SSL_CERT] = (string) $this->config['ssl_cert'];
}
if (array_key_exists('ssl_ca', $this->config)
&& !empty($this->config['ssl_ca'])
&& defined('\PDO::MYSQL_ATTR_SSL_CA')
) {
$options[\PDO::MYSQL_ATTR_SSL_CA] = (string) $this->config['ssl_ca'];
}
if (array_key_exists('ssl_cipher', $this->config)
&& !empty($this->config['ssl_cipher'])
&& defined('\PDO::MYSQL_ATTR_SSL_CIPHER')
) {
$options[\PDO::MYSQL_ATTR_SSL_CIPHER] = (string) $this->config['ssl_cipher'];
}
if (array_key_exists('ssl_verify_server_cert', $this->config)
&& defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
) {
$options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (boolean) $this->config[ 'ssl_verify_server_cert' ];
}
try {
$this->driver = Driver::create($this->config['dsn'], $this->config['user'], $this->config['password'], $options);
} catch (\PDOException $e) {
$message = $e->getMessage();
if ($message === 'could not find driver') {
list ($missingDriver, ) = explode(':', $this->config['dsn'], 2);
$message = "could not find $missingDriver driver";
}
throw new ModuleException(__CLASS__, $message . ' while creating PDO connection');
}
if ($this->config['waitlock']) {
$this->driver->setWaitLock($this->config['waitlock']);
}
$this->debugSection('Db', 'Connected to ' . $this->driver->getDb());
$this->dbh = $this->driver->getDbh();
}
private function disconnect()
{
$this->driver = null;
$this->dbh = null;
$this->debugSection('Db', 'Disconnected');
}
public function _before(TestInterface $test)
{
if ($this->config['reconnect']) {
$this->disconnect();
$this->connect();
}
if ($this->config['cleanup'] && !$this->populated) {
$this->_cleanup();
$this->_loadDump();
}
parent::_before($test);
}
public function _after(TestInterface $test)
{
$this->removeInserted();
if ($this->config['reconnect']) {
$this->disconnect();
}
parent::_after($test);
}
protected function removeInserted()
{
foreach (array_reverse($this->insertedRows) as $row) {
try {
$this->driver->deleteQueryByCriteria($row['table'], $row['primary']);
} catch (\Exception $e) {
$this->debug("couldn't delete record " . json_encode($row['primary']) ." from {$row['table']}");
}
}
$this->insertedRows = [];
$this->populated = false;
}
public function _cleanup()
{
$dbh = $this->driver->getDbh();
if (!$dbh) {
throw new ModuleConfigException(
__CLASS__,
'No connection to database. Remove this module from config if you don\'t need database repopulation'
);
}
try {
// don't clear database for empty dump
if (!count($this->sql)) {
return;
}
$this->driver->cleanup();
$this->populated = false;
} catch (\Exception $e) {
throw new ModuleException(__CLASS__, $e->getMessage());
}
}
public function isPopulated()
{
return $this->populated;
}
public function _loadDump()
{
if ($this->config['populator']) {
$this->loadDumpUsingPopulator();
return;
}
$this->loadDumpUsingDriver();
}
protected function loadDumpUsingPopulator()
{
$populator = new DbPopulator($this->config);
$this->populated = $populator->run();
}
protected function loadDumpUsingDriver()
{
if (!$this->sql) {
$this->debugSection('Db', 'No SQL loaded, loading dump skipped');
return;
}
$this->driver->load($this->sql);
$this->populated = true;
}
/**
* Inserts an SQL record into a database. This record will be erased after the test.
*
* ```php
* <?php
* $I->haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com'));
* ?>
* ```
*
* @param string $table
* @param array $data
*
* @return integer $id
*/
public function haveInDatabase($table, array $data)
{
$lastInsertId = $this->_insertInDatabase($table, $data);
$this->addInsertedRow($table, $data, $lastInsertId);
return $lastInsertId;
}
public function _insertInDatabase($table, array $data)
{
$query = $this->driver->insert($table, $data);
$parameters = array_values($data);
$this->debugSection('Query', $query);
$this->debugSection('Parameters', $parameters);
$this->driver->executeQuery($query, $parameters);
try {
$lastInsertId = (int)$this->driver->lastInsertId($table);
} catch (\PDOException $e) {
// ignore errors due to uncommon DB structure,
// such as tables without _id_seq in PGSQL
$lastInsertId = 0;
}
return $lastInsertId;
}
private function addInsertedRow($table, array $row, $id)
{
$primaryKey = $this->driver->getPrimaryKey($table);
$primary = [];
if ($primaryKey) {
if ($id && count($primaryKey) === 1) {
$primary [$primaryKey[0]] = $id;
} else {
foreach ($primaryKey as $column) {
if (isset($row[$column])) {
$primary[$column] = $row[$column];
} else {
throw new \InvalidArgumentException(
'Primary key field ' . $column . ' is not set for table ' . $table
);
}
}
}
} else {
$primary = $row;
}
$this->insertedRows[] = [
'table' => $table,
'primary' => $primary,
];
}
public function seeInDatabase($table, $criteria = [])
{
$res = $this->countInDatabase($table, $criteria);
$this->assertGreaterThan(
0,
$res,
'No matching records found for criteria ' . json_encode($criteria) . ' in table ' . $table
);
}
/**
* Asserts that the given number of records were found in the database.
*
* ```php
* <?php
* $I->seeNumRecords(1, 'users', ['name' => 'davert'])
* ?>
* ```
*
* @param int $expectedNumber Expected number
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*/
public function seeNumRecords($expectedNumber, $table, array $criteria = [])
{
$actualNumber = $this->countInDatabase($table, $criteria);
$this->assertEquals(
$expectedNumber,
$actualNumber,
sprintf(
'The number of found rows (%d) does not match expected number %d for criteria %s in table %s',
$actualNumber,
$expectedNumber,
json_encode($criteria),
$table
)
);
}
public function dontSeeInDatabase($table, $criteria = [])
{
$count = $this->countInDatabase($table, $criteria);
$this->assertLessThan(
1,
$count,
'Unexpectedly found matching records for criteria ' . json_encode($criteria) . ' in table ' . $table
);
}
/**
* Count rows in a database
*
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*
* @return int
*/
protected function countInDatabase($table, array $criteria = [])
{
return (int) $this->proceedSeeInDatabase($table, 'count(*)', $criteria);
}
/**
* Fetches all values from the column in database.
* Provide table name, desired column and criteria.
*
* @param string $table
* @param string $column
* @param array $criteria
*
* @return array
*/
protected function proceedSeeInDatabase($table, $column, $criteria)
{
$query = $this->driver->select($column, $table, $criteria);
$parameters = array_values($criteria);
$this->debugSection('Query', $query);
if (!empty($parameters)) {
$this->debugSection('Parameters', $parameters);
}
$sth = $this->driver->executeQuery($query, $parameters);
return $sth->fetchColumn();
}
/**
* Fetches all values from the column in database.
* Provide table name, desired column and criteria.
*
* ``` php
* <?php
* $mails = $I->grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter'));
* ```
*
* @param string $table
* @param string $column
* @param array $criteria
*
* @return array
*/
public function grabColumnFromDatabase($table, $column, array $criteria = [])
{
$query = $this->driver->select($column, $table, $criteria);
$parameters = array_values($criteria);
$this->debugSection('Query', $query);
$this->debugSection('Parameters', $parameters);
$sth = $this->driver->executeQuery($query, $parameters);
return $sth->fetchAll(\PDO::FETCH_COLUMN, 0);
}
/**
* Fetches all values from the column in database.
* Provide table name, desired column and criteria.
*
* ``` php
* <?php
* $mails = $I->grabFromDatabase('users', 'email', array('name' => 'RebOOter'));
* ```
*
* @param string $table
* @param string $column
* @param array $criteria
*
* @return array
*/
public function grabFromDatabase($table, $column, $criteria = [])
{
return $this->proceedSeeInDatabase($table, $column, $criteria);
}
/**
* Returns the number of rows in a database
*
* @param string $table Table name
* @param array $criteria Search criteria [Optional]
*
* @return int
*/
public function grabNumRecords($table, array $criteria = [])
{
return $this->countInDatabase($table, $criteria);
}
/**
* Update an SQL record into a database.
*
* ```php
* <?php
* $I->updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com'));
* ?>
* ```
*
* @param string $table
* @param array $data
* @param array $criteria
*/
public function updateInDatabase($table, array $data, array $criteria = [])
{
$query = $this->driver->update($table, $data, $criteria);
$parameters = array_merge(array_values($data), array_values($criteria));
$this->debugSection('Query', $query);
if (!empty($parameters)) {
$this->debugSection('Parameters', $parameters);
}
$this->driver->executeQuery($query, $parameters);
}
}

View File

@@ -0,0 +1,529 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\DataMapper;
use Codeception\Module as CodeceptionModule;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Interfaces\DoctrineProvider;
use Codeception\TestInterface;
use Codeception\Util\Stub;
/**
* Access the database using [Doctrine2 ORM](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/).
*
* When used with Zend Framework 2 or Symfony2, Doctrine's Entity Manager is automatically retrieved from Service Locator.
* Set up your `functional.suite.yml` like this:
*
* ```
* modules:
* enabled:
* - Symfony # 'ZF2' or 'Symfony'
* - Doctrine2:
* depends: Symfony
* cleanup: true # All doctrine queries will be wrapped in a transaction, which will be rolled back at the end of each test
* ```
*
* If you don't use Symfony or Zend Framework, you need to specify a callback function to retrieve the Entity Manager:
*
* ```
* modules:
* enabled:
* - Doctrine2:
* connection_callback: ['MyDb', 'createEntityManager']
* cleanup: true # All doctrine queries will be wrapped in a transaction, which will be rolled back at the end of each test
*
* ```
*
* This will use static method of `MyDb::createEntityManager()` to establish the Entity Manager.
*
* By default, the module will wrap everything into a transaction for each test and roll it back afterwards. By doing this
* tests will run much faster and will be isolated from each other.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **stable**
* * Contact: codecept@davert.mail.ua
*
* ## Config
*
* ## Public Properties
*
* * `em` - Entity Manager
*/
class Doctrine2 extends CodeceptionModule implements DependsOnModule, DataMapper
{
protected $config = [
'cleanup' => true,
'connection_callback' => false,
'depends' => null
];
protected $dependencyMessage = <<<EOF
Provide connection_callback function to establish database connection and get Entity Manager:
modules:
enabled:
- Doctrine2:
connection_callback: [My\ConnectionClass, getEntityManager]
Or set a dependent module, which can be either Symfony or ZF2 to get EM from service locator:
modules:
enabled:
- Doctrine2:
depends: Symfony
EOF;
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
public $em = null;
/**
* @var \Codeception\Lib\Interfaces\DoctrineProvider
*/
private $dependentModule;
public function _depends()
{
if ($this->config['connection_callback']) {
return [];
}
return ['Codeception\Lib\Interfaces\DoctrineProvider' => $this->dependencyMessage];
}
public function _inject(DoctrineProvider $dependentModule = null)
{
$this->dependentModule = $dependentModule;
}
public function _beforeSuite($settings = [])
{
$this->retrieveEntityManager();
}
public function _before(TestInterface $test)
{
$this->retrieveEntityManager();
if ($this->config['cleanup']) {
$this->em->getConnection()->beginTransaction();
$this->debugSection('Database', 'Transaction started');
}
}
protected function retrieveEntityManager()
{
if ($this->dependentModule) {
$this->em = $this->dependentModule->_getEntityManager();
} else {
if (is_callable($this->config['connection_callback'])) {
$this->em = call_user_func($this->config['connection_callback']);
}
}
if (!$this->em) {
throw new ModuleConfigException(
__CLASS__,
"EntityManager can't be obtained.\n \n"
. "Please specify either `connection_callback` config option\n"
. "with callable which will return instance of EntityManager or\n"
. "pass a dependent module which are Symfony or ZF2\n"
. "to connect to Doctrine using Dependency Injection Container"
);
}
if (!($this->em instanceof \Doctrine\ORM\EntityManagerInterface)) {
throw new ModuleConfigException(
__CLASS__,
"Connection object is not an instance of \\Doctrine\\ORM\\EntityManagerInterface.\n"
. "Use `connection_callback` or dependent framework modules to specify one"
);
}
$this->em->getConnection()->connect();
}
public function _after(TestInterface $test)
{
if (!$this->em instanceof \Doctrine\ORM\EntityManagerInterface) {
return;
}
if ($this->config['cleanup'] && $this->em->getConnection()->isTransactionActive()) {
try {
$this->em->getConnection()->rollback();
$this->debugSection('Database', 'Transaction cancelled; all changes reverted.');
} catch (\PDOException $e) {
}
}
$this->clean();
$this->em->getConnection()->close();
}
protected function clean()
{
$em = $this->em;
$reflectedEm = new \ReflectionClass($em);
if ($reflectedEm->hasProperty('repositories')) {
$property = $reflectedEm->getProperty('repositories');
$property->setAccessible(true);
$property->setValue($em, []);
}
$this->em->clear();
}
/**
* Performs $em->flush();
*/
public function flushToDatabase()
{
$this->em->flush();
}
/**
* Adds entity to repository and flushes. You can redefine it's properties with the second parameter.
*
* Example:
*
* ``` php
* <?php
* $I->persistEntity(new \Entity\User, array('name' => 'Miles'));
* $I->persistEntity($user, array('name' => 'Miles'));
* ```
*
* @param $obj
* @param array $values
*/
public function persistEntity($obj, $values = [])
{
if ($values) {
$reflectedObj = new \ReflectionClass($obj);
foreach ($values as $key => $val) {
$property = $reflectedObj->getProperty($key);
$property->setAccessible(true);
$property->setValue($obj, $val);
}
}
$this->em->persist($obj);
$this->em->flush();
}
/**
* Mocks the repository.
*
* With this action you can redefine any method of any repository.
* Please, note: this fake repositories will be accessible through entity manager till the end of test.
*
* Example:
*
* ``` php
* <?php
*
* $I->haveFakeRepository('Entity\User', array('findByUsername' => function($username) { return null; }));
*
* ```
*
* This creates a stub class for Entity\User repository with redefined method findByUsername,
* which will always return the NULL value.
*
* @param $classname
* @param array $methods
*/
public function haveFakeRepository($classname, $methods = [])
{
$em = $this->em;
$metadata = $em->getMetadataFactory()->getMetadataFor($classname);
$customRepositoryClassName = $metadata->customRepositoryClassName;
if (!$customRepositoryClassName) {
$customRepositoryClassName = '\Doctrine\ORM\EntityRepository';
}
$mock = Stub::make(
$customRepositoryClassName,
array_merge(
[
'_entityName' => $metadata->name,
'_em' => $em,
'_class' => $metadata
],
$methods
)
);
$em->clear();
$reflectedEm = new \ReflectionClass($em);
if ($reflectedEm->hasProperty('repositories')) {
//Support doctrine versions before 2.4.0
$property = $reflectedEm->getProperty('repositories');
$property->setAccessible(true);
$property->setValue($em, array_merge($property->getValue($em), [$classname => $mock]));
} elseif ($reflectedEm->hasProperty('repositoryFactory')) {
//For doctrine 2.4.0+ versions
$repositoryFactoryProperty = $reflectedEm->getProperty('repositoryFactory');
$repositoryFactoryProperty->setAccessible(true);
$repositoryFactory = $repositoryFactoryProperty->getValue($em);
$reflectedRepositoryFactory = new \ReflectionClass($repositoryFactory);
if ($reflectedRepositoryFactory->hasProperty('repositoryList')) {
$repositoryListProperty = $reflectedRepositoryFactory->getProperty('repositoryList');
$repositoryListProperty->setAccessible(true);
$repositoryListProperty->setValue(
$repositoryFactory,
[$classname => $mock]
);
$repositoryFactoryProperty->setValue($em, $repositoryFactory);
} else {
$this->debugSection(
'Warning',
'Repository can\'t be mocked, the EventManager\'s repositoryFactory doesn\'t have "repositoryList" property'
);
}
} else {
$this->debugSection(
'Warning',
'Repository can\'t be mocked, the EventManager class doesn\'t have "repositoryFactory" or "repositories" property'
);
}
}
/**
* Persists record into repository.
* This method creates an entity, and sets its properties directly (via reflection).
* Setters of entity won't be executed, but you can create almost any entity and save it to database.
* Returns id using `getId` of newly created entity.
*
* ```php
* $I->haveInRepository('Entity\User', array('name' => 'davert'));
* ```
*/
public function haveInRepository($entity, array $data)
{
$reflectedEntity = new \ReflectionClass($entity);
$entityObject = $reflectedEntity->newInstance();
foreach ($reflectedEntity->getProperties() as $property) {
/** @var $property \ReflectionProperty */
if (!isset($data[$property->name])) {
continue;
}
$property->setAccessible(true);
$property->setValue($entityObject, $data[$property->name]);
}
$this->em->persist($entityObject);
$this->em->flush();
if (method_exists($entityObject, 'getId')) {
$id = $entityObject->getId();
$this->debug("$entity entity created with id:$id");
return $id;
}
}
/**
* Flushes changes to database, and executes a query with parameters defined in an array.
* You can use entity associations to build complex queries.
*
* Example:
*
* ``` php
* <?php
* $I->seeInRepository('AppBundle:User', array('name' => 'davert'));
* $I->seeInRepository('User', array('name' => 'davert', 'Company' => array('name' => 'Codegyre')));
* $I->seeInRepository('Client', array('User' => array('Company' => array('name' => 'Codegyre')));
* ?>
* ```
*
* Fails if record for given criteria can\'t be found,
*
* @param $entity
* @param array $params
*/
public function seeInRepository($entity, $params = [])
{
$res = $this->proceedSeeInRepository($entity, $params);
$this->assert($res);
}
/**
* Flushes changes to database and performs `findOneBy()` call for current repository.
*
* @param $entity
* @param array $params
*/
public function dontSeeInRepository($entity, $params = [])
{
$res = $this->proceedSeeInRepository($entity, $params);
$this->assertNot($res);
}
protected function proceedSeeInRepository($entity, $params = [])
{
// we need to store to database...
$this->em->flush();
$data = $this->em->getClassMetadata($entity);
$qb = $this->em->getRepository($entity)->createQueryBuilder('s');
$this->buildAssociationQuery($qb, $entity, 's', $params);
$this->debug($qb->getDQL());
$res = $qb->getQuery()->getArrayResult();
return ['True', (count($res) > 0), "$entity with " . json_encode($params)];
}
/**
* Selects field value from repository.
* It builds query based on array of parameters.
* You can use entity associations to build complex queries.
*
* Example:
*
* ``` php
* <?php
* $email = $I->grabFromRepository('User', 'email', array('name' => 'davert'));
* ?>
* ```
*
* @version 1.1
* @param $entity
* @param $field
* @param array $params
* @return array
*/
public function grabFromRepository($entity, $field, $params = [])
{
// we need to store to database...
$this->em->flush();
$data = $this->em->getClassMetadata($entity);
$qb = $this->em->getRepository($entity)->createQueryBuilder('s');
$qb->select('s.' . $field);
$this->buildAssociationQuery($qb, $entity, 's', $params);
$this->debug($qb->getDQL());
return $qb->getQuery()->getSingleScalarResult();
}
/**
* Selects entities from repository.
* It builds query based on array of parameters.
* You can use entity associations to build complex queries.
*
* Example:
*
* ``` php
* <?php
* $users = $I->grabEntitiesFromRepository('AppBundle:User', array('name' => 'davert'));
* ?>
* ```
*
* @version 1.1
* @param $entity
* @param array $params
* @return array
*/
public function grabEntitiesFromRepository($entity, $params = [])
{
// we need to store to database...
$this->em->flush();
$data = $this->em->getClassMetadata($entity);
$qb = $this->em->getRepository($entity)->createQueryBuilder('s');
$qb->select('s');
$this->buildAssociationQuery($qb, $entity, 's', $params);
$this->debug($qb->getDQL());
return $qb->getQuery()->getResult();
}
/**
* Selects a single entity from repository.
* It builds query based on array of parameters.
* You can use entity associations to build complex queries.
*
* Example:
*
* ``` php
* <?php
* $user = $I->grabEntityFromRepository('User', array('id' => '1234'));
* ?>
* ```
*
* @version 1.1
* @param $entity
* @param array $params
* @return object
*/
public function grabEntityFromRepository($entity, $params = [])
{
// we need to store to database...
$this->em->flush();
$data = $this->em->getClassMetadata($entity);
$qb = $this->em->getRepository($entity)->createQueryBuilder('s');
$qb->select('s');
$this->buildAssociationQuery($qb, $entity, 's', $params);
$this->debug($qb->getDQL());
return $qb->getQuery()->getSingleResult();
}
/**
* It's Fuckin Recursive!
*
* @param $qb
* @param $assoc
* @param $alias
* @param $params
*/
protected function buildAssociationQuery($qb, $assoc, $alias, $params)
{
$data = $this->em->getClassMetadata($assoc);
foreach ($params as $key => $val) {
if (isset($data->associationMappings)) {
if ($map = array_key_exists($key, $data->associationMappings)) {
if (is_array($val)) {
$qb->innerJoin("$alias.$key", "_$key");
foreach ($val as $column => $v) {
if (is_array($v)) {
$this->buildAssociationQuery($qb, $map['targetEntity'], $column, $v);
continue;
}
$paramname = "_$key" . '__' . $column;
$qb->andWhere("_$key.$column = :$paramname");
$qb->setParameter($paramname, $v);
}
continue;
}
}
}
if ($val === null) {
$qb->andWhere("s.$key IS NULL");
} else {
$paramname = str_replace(".", "", "s_$key");
$qb->andWhere("s.$key = :$paramname");
$qb->setParameter($paramname, $val);
}
}
}
public function _getEntityManager()
{
if (is_null($this->em)) {
$this->retrieveEntityManager();
}
return $this->em;
}
}

View File

@@ -0,0 +1,883 @@
<?php
namespace Codeception\Module;
use Codeception\TestInterface;
/**
*
* Works with SFTP/FTP servers.
*
* In order to test the contents of a specific file stored on any remote FTP/SFTP system
* this module downloads a temporary file to the local system. The temporary directory is
* defined by default as ```tests/_data``` to specify a different directory set the tmp config
* option to your chosen path.
*
* Don't forget to create the folder and ensure its writable.
*
* Supported and tested FTP types are:
*
* * FTP
* * SFTP
*
* Connection uses php build in FTP client for FTP,
* connection to SFTP uses [phpseclib](http://phpseclib.sourceforge.net/) pulled in using composer.
*
* For SFTP, add [phpseclib](http://phpseclib.sourceforge.net/) to require list.
* ```
* "require": {
* "phpseclib/phpseclib": "0.3.6"
* }
* ```
*
* ## Status
*
* * Maintainer: **nathanmac**
* * Stability:
* - FTP: **stable**
* - SFTP: **stable**
* * Contact: nathan.macnamara@outlook.com
*
* ## Config
*
* * type: ftp - type of connection ftp/sftp (defaults to ftp).
* * host *required* - hostname/ip address of the ftp server.
* * port: 21 - port number for the ftp server
* * timeout: 90 - timeout settings for connecting the ftp server.
* * user: anonymous - user to access ftp server, defaults to anonymous authentication.
* * password - password, defaults to empty for anonymous.
* * key - path to RSA key for sftp.
* * tmp - path to local directory for storing tmp files.
* * passive: true - Turns on or off passive mode (FTP only)
* * cleanup: true - remove tmp files from local directory on completion.
*
* ### Example
* #### Example (FTP)
*
* modules:
* enabled: [FTP]
* config:
* FTP:
* type: ftp
* host: '127.0.0.1'
* port: 21
* timeout: 120
* user: 'root'
* password: 'root'
* key: ~/.ssh/id_rsa
* tmp: 'tests/_data/ftp'
* passive: true
* cleanup: false
*
* #### Example (SFTP)
*
* modules:
* enabled: [FTP]
* config:
* FTP:
* type: sftp
* host: '127.0.0.1'
* port: 22
* timeout: 120
* user: 'root'
* password: 'root'
* key: ''
* tmp: 'tests/_data/ftp'
* cleanup: false
*
*
* This module extends the Filesystem module, file contents methods are inherited from this module.
*/
class FTP extends Filesystem
{
/**
* FTP/SFTP connection handler
*/
protected $ftp = null;
/**
* Configuration options and default settings
*
* @var array
*/
protected $config = [
'type' => 'ftp',
'port' => 21,
'timeout' => 90,
'user' => 'anonymous',
'password' => '',
'key' => '',
'tmp' => 'tests/_data',
'passive' => false,
'cleanup' => true
];
/**
* Required configuration fields
*
* @var array
*/
protected $requiredFields = ['host'];
// ----------- SETUP METHODS BELOW HERE -------------------------//
/**
* Setup connection and login with config settings
*
* @param \Codeception\TestInterface $test
*/
public function _before(TestInterface $test)
{
// Login using config settings
$this->loginAs($this->config['user'], $this->config['password']);
}
/**
* Close the FTP connection & Clear up
*/
public function _after(TestInterface $test)
{
$this->_closeConnection();
// Clean up temp files
if ($this->config['cleanup']) {
if (file_exists($this->config['tmp'] . '/ftp_data_file.tmp')) {
unlink($this->config['tmp'] . '/ftp_data_file.tmp');
}
}
}
/**
* Change the logged in user mid-way through your test, this closes the
* current connection to the server and initialises and new connection.
*
* On initiation of this modules you are automatically logged into
* the server using the specified config options or defaulted
* to anonymous user if not provided.
*
* ``` php
* <?php
* $I->loginAs('user','password');
* ?>
* ```
*
* @param String $user
* @param String $password
*/
public function loginAs($user = 'anonymous', $password = '')
{
$this->_openConnection($user, $password); // Create new connection and login.
}
/**
* Enters a directory on the ftp system - FTP root directory is used by default
*
* @param $path
*/
public function amInPath($path)
{
$this->_changeDirectory($this->path = $this->absolutizePath($path) . ($path == '/' ? '' : DIRECTORY_SEPARATOR));
$this->debug('Moved to ' . $this->path);
}
/**
* Resolve path
*
* @param $path
* @return string
*/
protected function absolutizePath($path)
{
if (strpos($path, '/') === 0) {
return $path;
}
return $this->path . $path;
}
// ----------- SEARCH METHODS BELOW HERE ------------------------//
/**
* Checks if file exists in path on the remote FTP/SFTP system.
* DOES NOT OPEN the file when it's exists
*
* ``` php
* <?php
* $I->seeFileFound('UserModel.php','app/models');
* ?>
* ```
*
* @param $filename
* @param string $path
*/
public function seeFileFound($filename, $path = '')
{
$files = $this->grabFileList($path);
$this->debug("see file: {$filename}");
$this->assertContains($filename, $files, "file {$filename} not found in {$path}");
}
/**
* Checks if file exists in path on the remote FTP/SFTP system, using regular expression as filename.
* DOES NOT OPEN the file when it's exists
*
* ``` php
* <?php
* $I->seeFileFoundMatches('/^UserModel_([0-9]{6}).php$/','app/models');
* ?>
* ```
*
* @param $regex
* @param string $path
*/
public function seeFileFoundMatches($regex, $path = '')
{
foreach ($this->grabFileList($path) as $filename) {
preg_match($regex, $filename, $matches);
if (!empty($matches)) {
$this->debug("file '{$filename}' matches '{$regex}'");
return;
}
}
$this->fail("no file matches found for '{$regex}'");
}
/**
* Checks if file does not exist in path on the remote FTP/SFTP system
*
* @param $filename
* @param string $path
*/
public function dontSeeFileFound($filename, $path = '')
{
$files = $this->grabFileList($path);
$this->debug("don't see file: {$filename}");
$this->assertNotContains($filename, $files);
}
/**
* Checks if file does not exist in path on the remote FTP/SFTP system, using regular expression as filename.
* DOES NOT OPEN the file when it's exists
*
* @param $regex
* @param string $path
*/
public function dontSeeFileFoundMatches($regex, $path = '')
{
foreach ($this->grabFileList($path) as $filename) {
preg_match($regex, $filename, $matches);
if (!empty($matches)) {
$this->fail("file matches found for {$regex}");
}
}
$this->assertTrue(true);
$this->debug("no files match '{$regex}'");
}
// ----------- UTILITY METHODS BELOW HERE -------------------------//
/**
* Opens a file (downloads from the remote FTP/SFTP system to a tmp directory for processing)
* and stores it's content.
*
* Usage:
*
* ``` php
* <?php
* $I->openFile('composer.json');
* $I->seeInThisFile('codeception/codeception');
* ?>
* ```
*
* @param $filename
*/
public function openFile($filename)
{
$this->_openFile($this->absolutizePath($filename));
}
/**
* Saves contents to tmp file and uploads the FTP/SFTP system.
* Overwrites current file on server if exists.
*
* ``` php
* <?php
* $I->writeToFile('composer.json', 'some data here');
* ?>
* ```
*
* @param $filename
* @param $contents
*/
public function writeToFile($filename, $contents)
{
$this->_writeToFile($this->absolutizePath($filename), $contents);
}
/**
* Create a directory on the server
*
* ``` php
* <?php
* $I->makeDir('vendor');
* ?>
* ```
*
* @param $dirname
*/
public function makeDir($dirname)
{
$this->makeDirectory($this->absolutizePath($dirname));
}
/**
* Currently not supported in this module, overwrite inherited method
*
* @param $src
* @param $dst
*/
public function copyDir($src, $dst)
{
$this->fail('copyDir() currently unsupported by FTP module');
}
/**
* Rename/Move file on the FTP/SFTP server
*
* ``` php
* <?php
* $I->renameFile('composer.lock', 'composer_old.lock');
* ?>
* ```
*
* @param $filename
* @param $rename
*/
public function renameFile($filename, $rename)
{
$this->renameDirectory($this->absolutizePath($filename), $this->absolutizePath($rename));
}
/**
* Rename/Move directory on the FTP/SFTP server
*
* ``` php
* <?php
* $I->renameDir('vendor', 'vendor_old');
* ?>
* ```
*
* @param $dirname
* @param $rename
*/
public function renameDir($dirname, $rename)
{
$this->renameDirectory($this->absolutizePath($dirname), $this->absolutizePath($rename));
}
/**
* Deletes a file on the remote FTP/SFTP system
*
* ``` php
* <?php
* $I->deleteFile('composer.lock');
* ?>
* ```
*
* @param $filename
*/
public function deleteFile($filename)
{
$this->delete($this->absolutizePath($filename));
}
/**
* Deletes directory with all subdirectories on the remote FTP/SFTP server
*
* ``` php
* <?php
* $I->deleteDir('vendor');
* ?>
* ```
*
* @param $dirname
*/
public function deleteDir($dirname)
{
$this->delete($this->absolutizePath($dirname));
}
/**
* Erases directory contents on the FTP/SFTP server
*
* ``` php
* <?php
* $I->cleanDir('logs');
* ?>
* ```
*
* @param $dirname
*/
public function cleanDir($dirname)
{
$this->clearDirectory($this->absolutizePath($dirname));
}
// ----------- GRABBER METHODS BELOW HERE -----------------------//
/**
* Grabber method for returning file/folders listing in an array
*
* ```php
* <?php
* $files = $I->grabFileList();
* $count = $I->grabFileList('TEST', false); // Include . .. .thumbs.db
* ?>
* ```
*
* @param string $path
* @param bool $ignore - suppress '.', '..' and '.thumbs.db'
* @return array
*/
public function grabFileList($path = '', $ignore = true)
{
$absolutize_path = $this->absolutizePath($path)
. ($path != '' && substr($path, -1) != '/' ? DIRECTORY_SEPARATOR : '');
$files = $this->_listFiles($absolutize_path);
$display_files = [];
if (is_array($files) && !empty($files)) {
$this->debug('File List:');
foreach ($files as &$file) {
if (strtolower($file) != '.' &&
strtolower($file) != '..' &&
strtolower($file) != 'thumbs.db'
) { // Ignore '.', '..' and 'thumbs.db'
// Replace full path from file listings if returned in listing
$file = str_replace(
$absolutize_path,
'',
$file
);
$display_files[] = $file;
$this->debug(' - ' . $file);
}
}
return $ignore ? $display_files : $files;
}
$this->debug("File List: <empty>");
return [];
}
/**
* Grabber method for returning file/folders count in directory
*
* ```php
* <?php
* $count = $I->grabFileCount();
* $count = $I->grabFileCount('TEST', false); // Include . .. .thumbs.db
* ?>
* ```
*
* @param string $path
* @param bool $ignore - suppress '.', '..' and '.thumbs.db'
* @return int
*/
public function grabFileCount($path = '', $ignore = true)
{
$count = count($this->grabFileList($path, $ignore));
$this->debug("File Count: {$count}");
return $count;
}
/**
* Grabber method to return file size
*
* ```php
* <?php
* $size = $I->grabFileSize('test.txt');
* ?>
* ```
*
* @param $filename
* @return bool
*/
public function grabFileSize($filename)
{
$fileSize = $this->size($filename);
$this->debug("{$filename} has a file size of {$fileSize}");
return $fileSize;
}
/**
* Grabber method to return last modified timestamp
*
* ```php
* <?php
* $time = $I->grabFileModified('test.txt');
* ?>
* ```
*
* @param $filename
* @return bool
*/
public function grabFileModified($filename)
{
$time = $this->modified($filename);
$this->debug("{$filename} was last modified at {$time}");
return $time;
}
/**
* Grabber method to return current working directory
*
* ```php
* <?php
* $pwd = $I->grabDirectory();
* ?>
* ```
*
* @return string
*/
public function grabDirectory()
{
$pwd = $this->_directory();
$this->debug("PWD: {$pwd}");
return $pwd;
}
// ----------- SERVER CONNECTION METHODS BELOW HERE -------------//
/**
* Open a new FTP/SFTP connection and authenticate user.
*
* @param string $user
* @param string $password
*/
private function _openConnection($user = 'anonymous', $password = '')
{
$this->_closeConnection(); // Close connection if already open
if ($this->isSFTP()) {
$this->sftpConnect($user, $password);
} else {
$this->ftpConnect($user, $password);
}
$pwd = $this->grabDirectory();
$this->path = $pwd . ($pwd == '/' ? '' : DIRECTORY_SEPARATOR);
}
/**
* Close open FTP/SFTP connection
*/
private function _closeConnection()
{
if (!$this->ftp) {
return;
}
if (!$this->isSFTP()) {
ftp_close($this->ftp);
$this->ftp = null;
}
}
/**
* Get the file listing for FTP/SFTP connection
*
* @param String $path
* @return array
*/
private function _listFiles($path)
{
if ($this->isSFTP()) {
$files = @$this->ftp->nlist($path);
} else {
$files = @ftp_nlist($this->ftp, $path);
}
if ($files === false) {
$this->fail("couldn't list files");
}
return $files;
}
/**
* Get the current directory for the FTP/SFTP connection
*
* @return string
*/
private function _directory()
{
if ($this->isSFTP()) {
// == DIRECTORY_SEPARATOR ? '' : $pwd;
$pwd = @$this->ftp->pwd();
} else {
$pwd = @ftp_pwd($this->ftp);
}
if (!$pwd) {
$this->fail("couldn't get current directory");
}
}
/**
* Change the working directory on the FTP/SFTP server
*
* @param $path
*/
private function _changeDirectory($path)
{
if ($this->isSFTP()) {
$changed = @$this->ftp->chdir($path);
} else {
$changed = @ftp_chdir($this->ftp, $path);
}
if (!$changed) {
$this->fail("couldn't change directory {$path}");
}
}
/**
* Download remote file to local tmp directory and open contents.
*
* @param $filename
*/
private function _openFile($filename)
{
// Check local tmp directory
if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) {
$this->fail('tmp directory not found or is not writable');
}
// Download file to local tmp directory
$tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp";
if ($this->isSFTP()) {
$downloaded = @$this->ftp->get($filename, $tmp_file);
} else {
$downloaded = @ftp_get($this->ftp, $tmp_file, $filename, FTP_BINARY);
}
if (!$downloaded) {
$this->fail('failed to download file to tmp directory');
}
// Open file content to variable
if ($this->file = file_get_contents($tmp_file)) {
$this->filepath = $filename;
} else {
$this->fail('failed to open tmp file');
}
}
/**
* Write data to local tmp file and upload to server
*
* @param $filename
* @param $contents
*/
private function _writeToFile($filename, $contents)
{
// Check local tmp directory
if (!is_dir($this->config['tmp']) || !is_writeable($this->config['tmp'])) {
$this->fail('tmp directory not found or is not writable');
}
// Build temp file
$tmp_file = $this->config['tmp'] . "/ftp_data_file.tmp";
file_put_contents($tmp_file, $contents);
// Update variables
$this->filepath = $tmp_file;
$this->file = $contents;
// Upload the file to server
if ($this->isSFTP()) {
$uploaded = @$this->ftp->put($filename, $tmp_file, NET_SFTP_LOCAL_FILE);
} else {
$uploaded = ftp_put($this->ftp, $filename, $tmp_file, FTP_BINARY);
}
if (!$uploaded) {
$this->fail('failed to upload file to server');
}
}
/**
* Make new directory on server
*
* @param $path
*/
private function makeDirectory($path)
{
if ($this->isSFTP()) {
$created = @$this->ftp->mkdir($path, true);
} else {
$created = @ftp_mkdir($this->ftp, $path);
}
if (!$created) {
$this->fail("couldn't make directory {$path}");
}
$this->debug("Make directory: {$path}");
}
/**
* Rename/Move directory/file on server
*
* @param $path
* @param $rename
*/
private function renameDirectory($path, $rename)
{
if ($this->isSFTP()) {
$renamed = @$this->ftp->rename($path, $rename);
} else {
$renamed = @ftp_rename($this->ftp, $path, $rename);
}
if (!$renamed) {
$this->fail("couldn't rename directory {$path} to {$rename}");
}
$this->debug("Renamed directory: {$path} to {$rename}");
}
/**
* Delete file on server
*
* @param $filename
*/
private function delete($filename, $isDir = false)
{
if ($this->isSFTP()) {
$deleted = @$this->ftp->delete($filename, $isDir);
} else {
$deleted = @$this->ftpDelete($filename);
}
if (!$deleted) {
$this->fail("couldn't delete {$filename}");
}
$this->debug("Deleted: {$filename}");
}
/**
* Function to recursively delete folder, used for PHP FTP build in client.
*
* @param $directory
* @return bool
*/
private function ftpDelete($directory)
{
// here we attempt to delete the file/directory
if (!(@ftp_rmdir($this->ftp, $directory) || @ftp_delete($this->ftp, $directory))) {
// if the attempt to delete fails, get the file listing
$filelist = @ftp_nlist($this->ftp, $directory);
// loop through the file list and recursively delete the FILE in the list
foreach ($filelist as $file) {
$this->ftpDelete($file);
}
// if the file list is empty, delete the DIRECTORY we passed
$this->ftpDelete($directory);
}
return true;
}
/**
* Clear directory on server of all content
*
* @param $path
*/
private function clearDirectory($path)
{
$this->debug("Clear directory: {$path}");
$this->delete($path);
$this->makeDirectory($path);
}
/**
* Return the size of a given file
*
* @param $filename
* @return bool
*/
private function size($filename)
{
if ($this->isSFTP()) {
$size = (int)@$this->ftp->size($filename);
} else {
$size = @ftp_size($this->ftp, $filename);
}
if ($size > 0) {
return $size;
}
$this->fail("couldn't get the file size for {$filename}");
}
/**
* Return the last modified time of a given file
*
* @param $filename
* @return bool
*/
private function modified($filename)
{
if ($this->isSFTP()) {
$info = @$this->ftp->lstat($filename);
if ($info) {
return $info['mtime'];
}
} else {
if ($time = @ftp_mdtm($this->ftp, $filename)) {
return $time;
}
}
$this->fail("couldn't get the file size for {$filename}");
}
/**
* @param $user
* @param $password
*/
protected function sftpConnect($user, $password)
{
$this->ftp = new \Net_SFTP($this->config['host'], $this->config['port'], $this->config['timeout']);
if ($this->ftp === false) {
$this->ftp = null;
$this->fail('failed to connect to ftp server');
}
if (isset($this->config['key'])) {
$keyFile = file_get_contents($this->config['key']);
$password = new \Crypt_RSA();
$password->loadKey($keyFile);
}
if (!$this->ftp->login($user, $password)) {
$this->fail('failed to authenticate user');
}
}
/**
* @param $user
* @param $password
*/
protected function ftpConnect($user, $password)
{
$this->ftp = ftp_connect($this->config['host'], $this->config['port'], $this->config['timeout']);
if ($this->ftp === false) {
$this->ftp = null;
$this->fail('failed to connect to ftp server');
}
// Login using given access details
if (!@ftp_login($this->ftp, $user, $password)) {
$this->fail('failed to authenticate user');
}
// Set passive mode option (ftp only option)
if (isset($this->config['passive'])) {
ftp_pasv($this->ftp, $this->config['passive']);
}
}
protected function isSFTP()
{
return strtolower($this->config['type']) == 'sftp';
}
}

View File

@@ -0,0 +1,325 @@
<?php
namespace Codeception\Module;
use Codeception\Exception\ModuleException as ModuleException;
use Codeception\Exception\ModuleConfigException as ModuleConfigException;
use Codeception\Lib\Driver\Facebook as FacebookDriver;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\Lib\Notification;
use Codeception\Module as BaseModule;
/**
* Provides testing for projects integrated with Facebook API.
* Relies on Facebook's tool Test User API.
*
* <div class="alert alert-info">
* To use this module with Composer you need <em>"facebook/php-sdk4": "5.*"</em> package.
* </div>
*
* ## Status
*
* [ ![Facebook Status for Codeception/Codeception](https://codeship.com/projects/e4bc90d0-1ed5-0134-566c-1ed679ae6c9d/status?branch=2.2)](https://codeship.com/projects/160201)
*
* * Stability: **beta**
* * Maintainer: **tiger-seo**
* * Contact: tiger.seo@codeception.com
*
* ## Config
*
* * app_id *required* - Facebook application ID
* * secret *required* - Facebook application secret
* * test_user - Facebook test user parameters:
* * name - You can specify a name for the test user you create. The specified name will also be used in the email address assigned to the test user.
* * locale - You can specify a locale for the test user you create, the default is en_US. The list of supported locales is available at https://www.facebook.com/translations/FacebookLocales.xml
* * permissions - An array of permissions. Your app is granted these permissions for the new test user. The full list of permissions is available at https://developers.facebook.com/docs/authentication/permissions
*
* ### Config example
*
* modules:
* enabled:
* - Facebook:
* depends: PhpBrowser
* app_id: 412345678901234
* secret: ccb79c1b0fdff54e4f7c928bf233aea5
* test_user:
* name: FacebookGuy
* locale: uk_UA
* permissions: [email, publish_stream]
*
* ### Test example:
*
* ``` php
* <?php
* $I = new ApiGuy($scenario);
* $I->am('Guest');
* $I->wantToTest('check-in to a place be published on the Facebook using API');
* $I->haveFacebookTestUserAccount();
* $accessToken = $I->grabFacebookTestUserAccessToken();
* $I->haveHttpHeader('Auth', 'FacebookToken ' . $accessToken);
* $I->amGoingTo('send request to the backend, so that it will publish on user\'s wall on Facebook');
* $I->sendPOST('/api/v1/some-api-endpoint');
* $I->seePostOnFacebookWithAttachedPlace('167724369950862');
*
* ```
*
* ``` php
* <?php
* $I = new WebGuy($scenario);
* $I->am('Guest');
* $I->wantToTest('log in to site using Facebook');
* $I->haveFacebookTestUserAccount(); // create facebook test user
* $I->haveTestUserLoggedInOnFacebook(); // so that facebook will not ask us for login and password
* $fbUserFirstName = $I->grabFacebookTestUserFirstName();
* $I->amOnPage('/welcome');
* $I->see('Welcome, Guest');
* $I->click('Login with Facebook');
* $I->see('Welcome, ' . $fbUserFirstName);
*
* ```
*
* @since 1.6.3
* @author tiger.seo@gmail.com
*/
class Facebook extends BaseModule implements DependsOnModule, RequiresPackage
{
protected $requiredFields = ['app_id', 'secret'];
/**
* @var FacebookDriver
*/
protected $facebook;
/**
* @var array
*/
protected $testUser = [];
/**
* @var PhpBrowser
*/
protected $browserModule;
protected $dependencyMessage = <<<EOF
Example configuring PhpBrowser
--
modules
enabled:
- Facebook:
depends: PhpBrowser
app_id: 412345678901234
secret: ccb79c1b0fdff54e4f7c928bf233aea5
test_user:
name: FacebookGuy
locale: uk_UA
permissions: [email, publish_stream]
EOF;
public function _requires()
{
return ['Facebook\Facebook' => '"facebook/graph-sdk": "~5.3"'];
}
public function _depends()
{
return ['Codeception\Module\PhpBrowser' => $this->dependencyMessage];
}
public function _inject(PhpBrowser $browserModule)
{
$this->browserModule = $browserModule;
}
protected function deleteTestUser()
{
if (array_key_exists('id', $this->testUser)) {
// make api-call for test user deletion
$this->facebook->deleteTestUser($this->testUser['id']);
$this->testUser = [];
}
}
public function _initialize()
{
Notification::deprecate('Facebook module is not maintained and will be deprecated. Contact Codeception team if you are interested in maintaining it');
if (!array_key_exists('test_user', $this->config)) {
$this->config['test_user'] = [
'permissions' => [],
'name' => 'Codeception Testuser'
];
} elseif (!array_key_exists('permissions', $this->config['test_user'])) {
$this->config['test_user']['permissions'] = [];
} elseif (!array_key_exists('name', $this->config['test_user'])) {
$this->config['test_user']['name'] = "codeception testuser";
}
$this->facebook = new FacebookDriver(
[
'app_id' => $this->config['app_id'],
'secret' => $this->config['secret'],
],
function ($title, $message) {
if (version_compare(PHP_VERSION, '5.4', '>=')) {
$this->debugSection($title, $message);
}
}
);
}
public function _afterSuite()
{
$this->deleteTestUser();
}
/**
* Get facebook test user be created.
*
* *Please, note that the test user is created only at first invoke, unless $renew arguments is true.*
*
* @param bool $renew true if the test user should be recreated
*/
public function haveFacebookTestUserAccount($renew = false)
{
if ($renew) {
$this->deleteTestUser();
}
// make api-call for test user creation only if it's not yet created
if (!array_key_exists('id', $this->testUser)) {
$this->testUser = $this->facebook->createTestUser(
$this->config['test_user']['name'],
$this->config['test_user']['permissions']
);
}
}
/**
* Get facebook test user be logged in on facebook.
* This is done by going to facebook.com
*
* @throws ModuleConfigException
*/
public function haveTestUserLoggedInOnFacebook()
{
if (!array_key_exists('id', $this->testUser)) {
throw new ModuleException(
__CLASS__,
'Facebook test user was not found. Did you forget to create one?'
);
}
$callbackUrl = $this->browserModule->_getUrl();
$this->browserModule->amOnUrl('https://facebook.com/login');
$this->browserModule->submitForm('#login_form', [
'email' => $this->grabFacebookTestUserEmail(),
'pass' => $this->grabFacebookTestUserPassword()
]);
// if login in successful we are back on login screen:
$this->browserModule->dontSeeInCurrentUrl('/login');
$this->browserModule->amOnUrl($callbackUrl);
}
/**
* Returns the test user access token.
*
* @return string
*/
public function grabFacebookTestUserAccessToken()
{
return $this->testUser['access_token'];
}
/**
* Returns the test user id.
*
* @return string
*/
public function grabFacebookTestUserId()
{
return $this->testUser['id'];
}
/**
* Returns the test user email.
*
* @return string
*/
public function grabFacebookTestUserEmail()
{
return $this->testUser['email'];
}
/**
* Returns URL for test user auto-login.
*
* @return string
*/
public function grabFacebookTestUserLoginUrl()
{
return $this->testUser['login_url'];
}
public function grabFacebookTestUserPassword()
{
return $this->testUser['password'];
}
/**
* Returns the test user name.
*
* @return string
*/
public function grabFacebookTestUserName()
{
if (!array_key_exists('profile', $this->testUser)) {
$this->testUser['profile'] = $this->facebook->getTestUserInfo($this->grabFacebookTestUserAccessToken());
}
return $this->testUser['profile']['name'];
}
/**
* Please, note that you must have publish_actions permission to be able to publish to user's feed.
*
* @param array $params
*/
public function postToFacebookAsTestUser($params)
{
$this->facebook->sendPostToFacebook($this->grabFacebookTestUserAccessToken(), $params);
}
/**
*
* Please, note that you must have publish_actions permission to be able to publish to user's feed.
*
* @param string $placeId Place identifier to be verified against user published posts
*/
public function seePostOnFacebookWithAttachedPlace($placeId)
{
$token = $this->grabFacebookTestUserAccessToken();
$this->debugSection('Access Token', $token);
$place = $this->facebook->getVisitedPlaceTagForTestUser($placeId, $token);
$this->assertEquals($placeId, $place['id'], "The place was not found on facebook page");
}
/**
*
* Please, note that you must have publish_actions permission to be able to publish to user's feed.
*
* @param string $message published post to be verified against the actual post on facebook
*/
public function seePostOnFacebookWithMessage($message)
{
$posts = $this->facebook->getLastPostsForTestUser($this->grabFacebookTestUserAccessToken());
$facebook_post_message = '';
$this->assertNotEquals($message, $facebook_post_message, "You can not test for an empty message post");
if ($posts['data']) {
foreach ($posts['data'] as $post) {
if (array_key_exists('message', $post) && ($post['message'] == $message)) {
$facebook_post_message = $post['message'];
}
}
}
$this->assertEquals($message, $facebook_post_message, "The post message was not found on facebook page");
}
}

View File

@@ -0,0 +1,343 @@
<?php
namespace Codeception\Module;
use Codeception\Util\FileSystem as Util;
use Symfony\Component\Finder\Finder;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
use Codeception\Configuration;
/**
* Module for testing local filesystem.
* Fork it to extend the module for FTP, Amazon S3, others.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **stable**
* * Contact: codecept@davert.mail.ua
*
* Module was developed to test Codeception itself.
*/
class Filesystem extends CodeceptionModule
{
protected $file = null;
protected $filepath = null;
protected $path = '';
public function _before(TestInterface $test)
{
$this->path = Configuration::projectDir();
}
/**
* Enters a directory In local filesystem.
* Project root directory is used by default
*
* @param string $path
*/
public function amInPath($path)
{
chdir($this->path = $this->absolutizePath($path) . DIRECTORY_SEPARATOR);
$this->debug('Moved to ' . getcwd());
}
/**
* @param string $path
* @return string
*/
protected function absolutizePath($path)
{
// *nix way
if (strpos($path, '/') === 0) {
return $path;
}
// windows
if (strpos($path, ':\\') === 1) {
return $path;
}
return $this->path . $path;
}
/**
* Opens a file and stores it's content.
*
* Usage:
*
* ``` php
* <?php
* $I->openFile('composer.json');
* $I->seeInThisFile('codeception/codeception');
* ?>
* ```
*
* @param string $filename
*/
public function openFile($filename)
{
$this->file = file_get_contents($this->absolutizePath($filename));
$this->filepath = $filename;
}
/**
* Deletes a file
*
* ``` php
* <?php
* $I->deleteFile('composer.lock');
* ?>
* ```
*
* @param string $filename
*/
public function deleteFile($filename)
{
if (!file_exists($this->absolutizePath($filename))) {
\PHPUnit\Framework\Assert::fail('file not found');
}
unlink($this->absolutizePath($filename));
}
/**
* Deletes directory with all subdirectories
*
* ``` php
* <?php
* $I->deleteDir('vendor');
* ?>
* ```
*
* @param string $dirname
*/
public function deleteDir($dirname)
{
$dir = $this->absolutizePath($dirname);
Util::deleteDir($dir);
}
/**
* Copies directory with all contents
*
* ``` php
* <?php
* $I->copyDir('vendor','old_vendor');
* ?>
* ```
*
* @param string $src
* @param string $dst
*/
public function copyDir($src, $dst)
{
Util::copyDir($src, $dst);
}
/**
* Checks If opened file has `text` in it.
*
* Usage:
*
* ``` php
* <?php
* $I->openFile('composer.json');
* $I->seeInThisFile('codeception/codeception');
* ?>
* ```
*
* @param string $text
*/
public function seeInThisFile($text)
{
$this->assertContains($text, $this->file, "No text '$text' in currently opened file");
}
/**
* Checks If opened file has the `number` of new lines.
*
* Usage:
*
* ``` php
* <?php
* $I->openFile('composer.json');
* $I->seeNumberNewLines(5);
* ?>
* ```
*
* @param int $number New lines
*/
public function seeNumberNewLines($number)
{
$lines = preg_split('/\n|\r/', $this->file);
$this->assertTrue(
(int) $number === count($lines),
"The number of new lines does not match with $number"
);
}
/**
* Checks that contents of currently opened file matches $regex
*
* @param string $regex
*/
public function seeThisFileMatches($regex)
{
$this->assertRegExp($regex, $this->file, "Contents of currently opened file does not match '$regex'");
}
/**
* Checks the strict matching of file contents.
* Unlike `seeInThisFile` will fail if file has something more than expected lines.
* Better to use with HEREDOC strings.
* Matching is done after removing "\r" chars from file content.
*
* ``` php
* <?php
* $I->openFile('process.pid');
* $I->seeFileContentsEqual('3192');
* ?>
* ```
*
* @param string $text
*/
public function seeFileContentsEqual($text)
{
$file = str_replace("\r", '', $this->file);
\PHPUnit\Framework\Assert::assertEquals($text, $file);
}
/**
* Checks If opened file doesn't contain `text` in it
*
* ``` php
* <?php
* $I->openFile('composer.json');
* $I->dontSeeInThisFile('codeception/codeception');
* ?>
* ```
*
* @param string $text
*/
public function dontSeeInThisFile($text)
{
$this->assertNotContains($text, $this->file, "Found text '$text' in currently opened file");
}
/**
* Deletes a file
*/
public function deleteThisFile()
{
$this->deleteFile($this->filepath);
}
/**
* Checks if file exists in path.
* Opens a file when it's exists
*
* ``` php
* <?php
* $I->seeFileFound('UserModel.php','app/models');
* ?>
* ```
*
* @param string $filename
* @param string $path
*/
public function seeFileFound($filename, $path = '')
{
if ($path === '' && file_exists($filename)) {
$this->openFile($filename);
\PHPUnit\Framework\Assert::assertFileExists($filename);
return;
}
$found = $this->findFileInPath($filename, $path);
if ($found === false) {
$this->fail("File \"$filename\" not found at \"$path\"");
}
$this->openFile($found);
\PHPUnit\Framework\Assert::assertFileExists($found);
}
/**
* Checks if file does not exist in path
*
* @param string $filename
* @param string $path
*/
public function dontSeeFileFound($filename, $path = '')
{
if ($path === '') {
\PHPUnit\Framework\Assert::assertFileNotExists($filename);
return;
}
$found = $this->findFileInPath($filename, $path);
if ($found === false) {
//this line keeps a count of assertions correct
\PHPUnit\Framework\Assert::assertTrue(true);
return;
}
\PHPUnit\Framework\Assert::assertFileNotExists($found);
}
/**
* Finds the first matching file
*
* @param string $filename
* @param string $path
* @throws \PHPUnit\Framework\AssertionFailedError When path does not exist
* @return string|false Path to the first matching file
*/
private function findFileInPath($filename, $path)
{
$path = $this->absolutizePath($path);
if (!file_exists($path)) {
$this->fail("Directory does not exist: $path");
}
$files = Finder::create()->files()->name($filename)->in($path);
if ($files->count() === 0) {
return false;
}
foreach ($files as $file) {
return $file->getRealPath();
}
}
/**
* Erases directory contents
*
* ``` php
* <?php
* $I->cleanDir('logs');
* ?>
* ```
*
* @param string $dirname
*/
public function cleanDir($dirname)
{
$path = $this->absolutizePath($dirname);
Util::doEmptyDir($path);
}
/**
* Saves contents to file
*
* @param string $filename
* @param string $contents
*/
public function writeToFile($filename, $contents)
{
file_put_contents($filename, $contents);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,575 @@
<?php
namespace Codeception\Module;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Connector\Lumen as LumenConnector;
use Codeception\Lib\Framework;
use Codeception\Lib\Interfaces\ActiveRecord;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\Lib\Shared\LaravelCommon;
use Codeception\Lib\ModuleContainer;
use Codeception\TestInterface;
use Codeception\Util\ReflectionHelper;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model as EloquentModel;
/**
*
* This module allows you to run functional tests for Lumen.
* Please try it and leave your feedback.
*
* ## Demo project
* <https://github.com/janhenkgerritsen/codeception-lumen-sample>
*
* ## Status
*
* * Maintainer: **Jan-Henk Gerritsen**
* * Stability: **dev**
* * Contact: janhenkgerritsen@gmail.com
*
* ## Config
*
* * cleanup: `boolean`, default `true` - all database queries will be run in a transaction,
* which will be rolled back at the end of each test.
* * bootstrap: `string`, default `bootstrap/app.php` - relative path to app.php config file.
* * root: `string`, default `` - root path of the application.
* * packages: `string`, default `workbench` - root path of application packages (if any).
* * url: `string`, default `http://localhost` - the application URL
*
* ## API
*
* * app - `\Laravel\Lumen\Application`
* * config - `array`
*
* ## Parts
*
* * ORM - only include the database methods of this module:
* * have
* * haveMultiple
* * haveRecord
* * grabRecord
* * seeRecord
* * dontSeeRecord
*/
class Lumen extends Framework implements ActiveRecord, PartedModule
{
use LaravelCommon;
/**
* @var \Laravel\Lumen\Application
*/
public $app;
/**
* @var array
*/
public $config = [];
/**
* Constructor.
*
* @param ModuleContainer $container
* @param array|null $config
*/
public function __construct(ModuleContainer $container, $config = null)
{
$this->config = array_merge(
[
'cleanup' => true,
'bootstrap' => 'bootstrap' . DIRECTORY_SEPARATOR . 'app.php',
'root' => '',
'packages' => 'workbench',
'url' => 'http://localhost',
],
(array)$config
);
$projectDir = explode($this->config['packages'], Configuration::projectDir())[0];
$projectDir .= $this->config['root'];
$this->config['project_dir'] = $projectDir;
$this->config['bootstrap_file'] = $projectDir . $this->config['bootstrap'];
parent::__construct($container);
}
/**
* @return array
*/
public function _parts()
{
return ['orm'];
}
/**
* Initialize hook.
*/
public function _initialize()
{
$this->checkBootstrapFileExists();
$this->registerAutoloaders();
}
/**
* Before hook.
*
* @param \Codeception\TestInterface $test
* @throws ModuleConfigException
*/
public function _before(TestInterface $test)
{
$this->client = new LumenConnector($this);
if ($this->app['db'] && $this->config['cleanup']) {
$this->app['db']->beginTransaction();
}
}
/**
* After hook.
*
* @param \Codeception\TestInterface $test
*/
public function _after(TestInterface $test)
{
if ($this->app['db'] && $this->config['cleanup']) {
$this->app['db']->rollback();
}
// disconnect from DB to prevent "Too many connections" issue
if ($this->app['db']) {
$this->app['db']->disconnect();
}
}
/**
* Make sure the Lumen bootstrap file exists.
*
* @throws ModuleConfigException
*/
protected function checkBootstrapFileExists()
{
$bootstrapFile = $this->config['bootstrap_file'];
if (!file_exists($bootstrapFile)) {
throw new ModuleConfigException(
$this,
"Lumen bootstrap file not found in $bootstrapFile.\n"
. "Please provide a valid path using the 'bootstrap' config param. "
);
}
}
/**
* Register autoloaders.
*/
protected function registerAutoloaders()
{
require $this->config['project_dir'] . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
}
/**
* Provides access the Lumen application object.
*
* @return \Laravel\Lumen\Application
*/
public function getApplication()
{
return $this->app;
}
/**
* @param \Laravel\Lumen\Application $app
*/
public function setApplication($app)
{
$this->app = $app;
}
/**
* Opens web page using route name and parameters.
*
* ```php
* <?php
* $I->amOnRoute('homepage');
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function amOnRoute($routeName, $params = [])
{
$route = $this->getRouteByName($routeName);
if (!$route) {
$this->fail("Could not find route with name '$routeName'");
}
$url = $this->generateUrlForRoute($route, $params);
$this->amOnPage($url);
}
/**
* Get the route for a route name.
*
* @param string $routeName
* @return array|null
*/
private function getRouteByName($routeName)
{
foreach ($this->app->getRoutes() as $route) {
if ($route['method'] != 'GET') {
return;
}
if (isset($route['action']['as']) && $route['action']['as'] == $routeName) {
return $route;
}
}
return null;
}
/**
* Generate the URL for a route specification.
* Replaces the route parameters from left to right with the parameters
* passed in the $params array.
*
* @param array $route
* @param array $params
* @return string
*/
private function generateUrlForRoute($route, $params)
{
$url = $route['uri'];
while (count($params) > 0) {
$param = array_shift($params);
$url = preg_replace('/{.+?}/', $param, $url, 1);
}
return $url;
}
/**
* Set the authenticated user for the next request.
* This will not persist between multiple requests.
*
* @param \Illuminate\Contracts\Auth\Authenticatable
* @param string|null $driver The authentication driver for Lumen <= 5.1.*, guard name for Lumen >= 5.2
* @return void
*/
public function amLoggedAs($user, $driver = null)
{
if (!$user instanceof Authenticatable) {
$this->fail(
'The user passed to amLoggedAs() should be an instance of \\Illuminate\\Contracts\\Auth\\Authenticatable'
);
}
$guard = $auth = $this->app['auth'];
if (method_exists($auth, 'driver')) {
$guard = $auth->driver($driver);
}
if (method_exists($auth, 'guard')) {
$guard = $auth->guard($driver);
}
$guard->setUser($user);
}
/**
* Checks that user is authenticated.
*/
public function seeAuthentication()
{
$this->assertTrue($this->app['auth']->check(), 'User is not logged in');
}
/**
* Check that user is not authenticated.
*/
public function dontSeeAuthentication()
{
$this->assertFalse($this->app['auth']->check(), 'User is logged in');
}
/**
* Return an instance of a class from the IoC Container.
*
* Example
* ``` php
* <?php
* // In Lumen
* App::bind('foo', function($app)
* {
* return new FooBar;
* });
*
* // Then in test
* $service = $I->grabService('foo');
*
* // Will return an instance of FooBar, also works for singletons.
* ?>
* ```
*
* @param string $class
* @return mixed
*/
public function grabService($class)
{
return $this->app[$class];
}
/**
* Inserts record into the database.
* If you pass the name of a database table as the first argument, this method returns an integer ID.
* You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model.
*
* ``` php
* <?php
* $user_id = $I->haveRecord('users', array('name' => 'Davert')); // returns integer
* $user = $I->haveRecord('App\User', array('name' => 'Davert')); // returns Eloquent model
* ?>
* ```
*
* @param string $table
* @param array $attributes
* @return integer|EloquentModel
* @part orm
*/
public function haveRecord($table, $attributes = [])
{
if (class_exists($table)) {
$model = new $table;
if (!$model instanceof EloquentModel) {
throw new \RuntimeException("Class $table is not an Eloquent model");
}
$model->fill($attributes)->save();
return $model;
}
try {
return $this->app['db']->table($table)->insertGetId($attributes);
} catch (\Exception $e) {
$this->fail("Could not insert record into table '$table':\n\n" . $e->getMessage());
}
}
/**
* Checks that record exists in database.
* You can pass the name of a database table or the class name of an Eloquent model as the first argument.
*
* ``` php
* <?php
* $I->seeRecord('users', array('name' => 'davert'));
* $I->seeRecord('App\User', array('name' => 'davert'));
* ?>
* ```
*
* @param string $table
* @param array $attributes
* @part orm
*/
public function seeRecord($table, $attributes = [])
{
if (class_exists($table)) {
if (!$this->findModel($table, $attributes)) {
$this->fail("Could not find $table with " . json_encode($attributes));
}
} elseif (!$this->findRecord($table, $attributes)) {
$this->fail("Could not find matching record in table '$table'");
}
}
/**
* Checks that record does not exist in database.
* You can pass the name of a database table or the class name of an Eloquent model as the first argument.
*
* ``` php
* <?php
* $I->dontSeeRecord('users', array('name' => 'davert'));
* $I->dontSeeRecord('App\User', array('name' => 'davert'));
* ?>
* ```
*
* @param string $table
* @param array $attributes
* @part orm
*/
public function dontSeeRecord($table, $attributes = [])
{
if (class_exists($table)) {
if ($this->findModel($table, $attributes)) {
$this->fail("Unexpectedly found matching $table with " . json_encode($attributes));
}
} elseif ($this->findRecord($table, $attributes)) {
$this->fail("Unexpectedly found matching record in table '$table'");
}
}
/**
* Retrieves record from database
* If you pass the name of a database table as the first argument, this method returns an array.
* You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model.
*
* ``` php
* <?php
* $record = $I->grabRecord('users', array('name' => 'davert')); // returns array
* $record = $I->grabRecord('App\User', array('name' => 'davert')); // returns Eloquent model
* ?>
* ```
*
* @param string $table
* @param array $attributes
* @return array|EloquentModel
* @part orm
*/
public function grabRecord($table, $attributes = [])
{
if (class_exists($table)) {
if (!$model = $this->findModel($table, $attributes)) {
$this->fail("Could not find $table with " . json_encode($attributes));
}
return $model;
}
if (!$record = $this->findRecord($table, $attributes)) {
$this->fail("Could not find matching record in table '$table'");
}
return $record;
}
/**
* @param string $modelClass
* @param array $attributes
*
* @return EloquentModel
*/
protected function findModel($modelClass, $attributes = [])
{
$model = new $modelClass;
if (!$model instanceof EloquentModel) {
throw new \RuntimeException("Class $modelClass is not an Eloquent model");
}
$query = $model->newQuery();
foreach ($attributes as $key => $value) {
$query->where($key, $value);
}
return $query->first();
}
/**
* @param string $table
* @param array $attributes
* @return array
*/
protected function findRecord($table, $attributes = [])
{
$query = $this->app['db']->table($table);
foreach ($attributes as $key => $value) {
$query->where($key, $value);
}
return (array)$query->first();
}
/**
* Use Lumen's model factory to create a model.
* Can only be used with Lumen 5.1 and later.
*
* ``` php
* <?php
* $I->have('App\User');
* $I->have('App\User', ['name' => 'John Doe']);
* $I->have('App\User', [], 'admin');
* ?>
* ```
*
* @see https://lumen.laravel.com/docs/master/testing#model-factories
* @param string $model
* @param array $attributes
* @param string $name
* @return mixed
* @part orm
*/
public function have($model, $attributes = [], $name = 'default')
{
try {
return $this->modelFactory($model, $name)->create($attributes);
} catch (\Exception $e) {
$this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage());
}
}
/**
* Use Laravel's model factory to create multiple models.
* Can only be used with Lumen 5.1 and later.
*
* ``` php
* <?php
* $I->haveMultiple('App\User', 10);
* $I->haveMultiple('App\User', 10, ['name' => 'John Doe']);
* $I->haveMultiple('App\User', 10, [], 'admin');
* ?>
* ```
*
* @see https://lumen.laravel.com/docs/master/testing#model-factories
* @param string $model
* @param int $times
* @param array $attributes
* @param string $name
* @return mixed
* @part orm
*/
public function haveMultiple($model, $times, $attributes = [], $name = 'default')
{
try {
return $this->modelFactory($model, $name, $times)->create($attributes);
} catch (\Exception $e) {
$this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage());
}
}
/**
* @param string $model
* @param string $name
* @param int $times
* @return \Illuminate\Database\Eloquent\FactoryBuilder
* @throws ModuleException
*/
protected function modelFactory($model, $name, $times = 1)
{
if (!function_exists('factory')) {
throw new ModuleException($this, 'The factory() method does not exist. ' .
'This functionality relies on Lumen model factories, which were introduced in Lumen 5.1.');
}
return factory($model, $name, $times);
}
/**
* Returns a list of recognized domain names.
* This elements of this list are regular expressions.
*
* @return array
*/
protected function getInternalDomains()
{
$server = ReflectionHelper::readPrivateProperty($this->client, 'server');
return ['/^' . str_replace('.', '\.', $server['HTTP_HOST']) . '$/'];
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
use Codeception\Exception\ModuleConfigException;
/**
* Connects to [memcached](http://www.memcached.org/) using either _Memcache_ or _Memcached_ extension.
*
* Performs a cleanup by flushing all values after each test run.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **beta**
* * Contact: davert@codeception.com
*
* ## Configuration
*
* * **`host`** (`string`, default `'localhost'`) - The memcached host
* * **`port`** (`int`, default `11211`) - The memcached port
*
* ### Example (`unit.suite.yml`)
*
* ```yaml
* modules:
* - Memcache:
* host: 'localhost'
* port: 11211
* ```
*
* Be sure you don't use the production server to connect.
*
* ## Public Properties
*
* * **memcache** - instance of _Memcache_ or _Memcached_ object
*
*/
class Memcache extends CodeceptionModule
{
/**
* @var \Memcache|\Memcached
*/
public $memcache = null;
/**
* {@inheritdoc}
*/
protected $config = [
'host' => 'localhost',
'port' => 11211
];
/**
* Code to run before each test.
*
* @param TestInterface $test
* @throws ModuleConfigException
*/
public function _before(TestInterface $test)
{
if (class_exists('\Memcache')) {
$this->memcache = new \Memcache;
$this->memcache->connect($this->config['host'], $this->config['port']);
} elseif (class_exists('\Memcached')) {
$this->memcache = new \Memcached;
$this->memcache->addServer($this->config['host'], $this->config['port']);
} else {
throw new ModuleConfigException(__CLASS__, 'Memcache classes not loaded');
}
}
/**
* Code to run after each test.
*
* @param TestInterface $test
*/
public function _after(TestInterface $test)
{
if (empty($this->memcache)) {
return;
}
$this->memcache->flush();
switch (get_class($this->memcache)) {
case 'Memcache':
$this->memcache->close();
break;
case 'Memcached':
$this->memcache->quit();
break;
}
}
/**
* Grabs value from memcached by key.
*
* Example:
*
* ``` php
* <?php
* $users_count = $I->grabValueFromMemcached('users_count');
* ?>
* ```
*
* @param $key
* @return array|string
*/
public function grabValueFromMemcached($key)
{
$value = $this->memcache->get($key);
$this->debugSection("Value", $value);
return $value;
}
/**
* Checks item in Memcached exists and the same as expected.
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key exists
* $I->seeInMemcached('users_count');
*
* // Checks a 'users_count' exists and has the value 200
* $I->seeInMemcached('users_count', 200);
* ?>
* ```
*
* @param $key
* @param $value
*/
public function seeInMemcached($key, $value = null)
{
$actual = $this->memcache->get($key);
$this->debugSection("Value", $actual);
if (null === $value) {
$this->assertNotFalse($actual, "Cannot find key '$key' in Memcached");
} else {
$this->assertEquals($value, $actual, "Cannot find key '$key' in Memcached with the provided value");
}
}
/**
* Checks item in Memcached doesn't exist or is the same as expected.
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key does not exist
* $I->dontSeeInMemcached('users_count');
*
* // Checks a 'users_count' exists does not exist or its value is not the one provided
* $I->dontSeeInMemcached('users_count', 200);
* ?>
* ```
*
* @param $key
* @param $value
*/
public function dontSeeInMemcached($key, $value = null)
{
$actual = $this->memcache->get($key);
$this->debugSection("Value", $actual);
if (null === $value) {
$this->assertFalse($actual, "The key '$key' exists in Memcached");
} else {
if (false !== $actual) {
$this->assertEquals($value, $actual, "The key '$key' exists in Memcached with the provided value");
}
}
}
/**
* Stores an item `$value` with `$key` on the Memcached server.
*
* @param string $key
* @param mixed $value
* @param int $expiration
*/
public function haveInMemcached($key, $value, $expiration = null)
{
switch (get_class($this->memcache)) {
case 'Memcache':
$this->assertTrue($this->memcache->set($key, $value, null, $expiration));
break;
case 'Memcached':
$this->assertTrue($this->memcache->set($key, $value, $expiration));
break;
}
}
/**
* Flushes all Memcached data.
*/
public function clearMemcache()
{
$this->memcache->flush();
}
}

View File

@@ -0,0 +1,441 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\Module as CodeceptionModule;
use Codeception\Configuration as Configuration;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Driver\MongoDb as MongoDbDriver;
use Codeception\TestInterface;
/**
* Works with MongoDb database.
*
* The most important function of this module is cleaning database before each test.
* To have your database properly cleaned you should configure it to access the database.
*
* In order to have your database populated with data you need a valid js file with data (of the same style which can be fed up to mongo binary)
* File can be generated by RockMongo export command
* You can also use directory, generated by ```mongodump``` tool or it's ```.tar.gz``` archive (not available for Windows systems), generated by ```tar -czf <archive_file_name>.tar.gz <path_to dump directory>```.
* Just put it in ``` tests/_data ``` dir (by default) and specify path to it in config.
* Next time after database is cleared all your data will be restored from dump.
* The DB preparation should as following:
* - clean database
* - system collection system.users should contain the user which will be authenticated while script performs DB operations
*
* Connection is done by MongoDb driver, which is stored in Codeception\Lib\Driver namespace.
* Check out the driver if you get problems loading dumps and cleaning databases.
*
* HINT: This module can be used with [Mongofill](https://github.com/mongofill/mongofill) library which is Mongo client written in PHP without extension.
*
* ## Status
*
* * Maintainer: **judgedim**, **davert**
* * Stability: **beta**
* * Contact: davert@codeception.com
*
* *Please review the code of non-stable modules and provide patches if you have issues.*
*
* ## Config
*
* * dsn *required* - MongoDb DSN with the db name specified at the end of the host after slash
* * user *required* - user to access database
* * password *required* - password
* * dump_type *required* - type of dump.
* One of 'js' (MongoDb::DUMP_TYPE_JS), 'mongodump' (MongoDb::DUMP_TYPE_MONGODUMP) or 'mongodump-tar-gz' (MongoDb::DUMP_TYPE_MONGODUMP_TAR_GZ).
* default: MongoDb::DUMP_TYPE_JS).
* * dump - path to database dump
* * populate: true - should the dump be loaded before test suite is started.
* * cleanup: true - should the dump be reloaded after each test
*
*/
class MongoDb extends CodeceptionModule implements RequiresPackage
{
const DUMP_TYPE_JS = 'js';
const DUMP_TYPE_MONGODUMP = 'mongodump';
const DUMP_TYPE_MONGODUMP_TAR_GZ = 'mongodump-tar-gz';
/**
* @api
* @var
*/
public $dbh;
/**
* @var
*/
protected $dumpFile;
protected $isDumpFileEmpty = true;
protected $config = [
'populate' => true,
'cleanup' => true,
'dump' => null,
'dump_type' => self::DUMP_TYPE_JS,
'user' => null,
'password' => null,
'quiet' => false,
];
protected $populated = false;
/**
* @var \Codeception\Lib\Driver\MongoDb
*/
public $driver;
protected $requiredFields = ['dsn'];
public function _initialize()
{
try {
$this->driver = MongoDbDriver::create(
$this->config['dsn'],
$this->config['user'],
$this->config['password']
);
} catch (\MongoConnectionException $e) {
throw new ModuleException(__CLASS__, $e->getMessage() . ' while creating Mongo connection');
}
// starting with loading dump
if ($this->config['populate']) {
$this->cleanup();
$this->loadDump();
$this->populated = true;
}
}
private function validateDump()
{
if ($this->config['dump'] && ($this->config['cleanup'] or ($this->config['populate']))) {
if (!file_exists(Configuration::projectDir() . $this->config['dump'])) {
throw new ModuleConfigException(
__CLASS__,
"File with dump doesn't exist.\n
Please, check path for dump file: " . $this->config['dump']
);
}
$this->dumpFile = Configuration::projectDir() . $this->config['dump'];
$this->isDumpFileEmpty = false;
if ($this->config['dump_type'] === self::DUMP_TYPE_JS) {
$content = file_get_contents($this->dumpFile);
$content = trim(preg_replace('%/\*(?:(?!\*/).)*\*/%s', "", $content));
if (!sizeof(explode("\n", $content))) {
$this->isDumpFileEmpty = true;
}
return;
}
if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP) {
if (!is_dir($this->dumpFile)) {
throw new ModuleConfigException(
__CLASS__,
"Dump must be a directory.\n
Please, check dump: " . $this->config['dump']
);
}
$this->isDumpFileEmpty = true;
$dumpDir = dir($this->dumpFile);
while (false !== ($entry = $dumpDir->read())) {
if ($entry !== '..' && $entry !== '.') {
$this->isDumpFileEmpty = false;
break;
}
}
$dumpDir->close();
return;
}
if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP_TAR_GZ) {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
throw new ModuleConfigException(
__CLASS__,
"Tar gunzip archives are not supported for Windows systems"
);
}
if (!preg_match('/(\.tar\.gz|\.tgz)$/', $this->dumpFile)) {
throw new ModuleConfigException(
__CLASS__,
"Dump file must be a valid tar gunzip archive.\n
Please, check dump file: " . $this->config['dump']
);
}
return;
}
throw new ModuleConfigException(
__CLASS__,
'\"dump_type\" must be one of ["'
. self::DUMP_TYPE_JS . '", "'
. self::DUMP_TYPE_MONGODUMP . '", "'
. self::DUMP_TYPE_MONGODUMP_TAR_GZ . '"].'
);
}
}
public function _before(TestInterface $test)
{
if ($this->config['cleanup'] && !$this->populated) {
$this->cleanup();
$this->loadDump();
}
}
public function _after(TestInterface $test)
{
$this->populated = false;
}
protected function cleanup()
{
$dbh = $this->driver->getDbh();
if (!$dbh) {
throw new ModuleConfigException(
__CLASS__,
"No connection to database. Remove this module from config if you don't need database repopulation"
);
}
try {
$this->driver->cleanup();
} catch (\Exception $e) {
throw new ModuleException(__CLASS__, $e->getMessage());
}
}
protected function loadDump()
{
$this->validateDump();
if ($this->isDumpFileEmpty) {
return;
}
try {
if ($this->config['dump_type'] === self::DUMP_TYPE_JS) {
$this->driver->load($this->dumpFile);
}
if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP) {
$this->driver->setQuiet($this->config['quiet']);
$this->driver->loadFromMongoDump($this->dumpFile);
}
if ($this->config['dump_type'] === self::DUMP_TYPE_MONGODUMP_TAR_GZ) {
$this->driver->setQuiet($this->config['quiet']);
$this->driver->loadFromTarGzMongoDump($this->dumpFile);
}
} catch (\Exception $e) {
throw new ModuleException(__CLASS__, $e->getMessage());
}
}
/**
* Specify the database to use
*
* ``` php
* <?php
* $I->useDatabase('db_1');
* ```
*
* @param $dbName
*/
public function useDatabase($dbName)
{
$this->driver->setDatabase($dbName);
}
/**
* Inserts data into collection
*
* ``` php
* <?php
* $I->haveInCollection('users', array('name' => 'John', 'email' => 'john@coltrane.com'));
* $user_id = $I->haveInCollection('users', array('email' => 'john@coltrane.com'));
* ```
*
* @param $collection
* @param array $data
*/
public function haveInCollection($collection, array $data)
{
$collection = $this->driver->getDbh()->selectCollection($collection);
if ($this->driver->isLegacy()) {
$collection->insert($data);
return $data['_id'];
}
$response = $collection->insertOne($data);
return (string) $response->getInsertedId();
}
/**
* Checks if collection contains an item.
*
* ``` php
* <?php
* $I->seeInCollection('users', array('name' => 'miles'));
* ```
*
* @param $collection
* @param array $criteria
*/
public function seeInCollection($collection, $criteria = [])
{
$collection = $this->driver->getDbh()->selectCollection($collection);
$res = $collection->count($criteria);
\PHPUnit\Framework\Assert::assertGreaterThan(0, $res);
}
/**
* Checks if collection doesn't contain an item.
*
* ``` php
* <?php
* $I->dontSeeInCollection('users', array('name' => 'miles'));
* ```
*
* @param $collection
* @param array $criteria
*/
public function dontSeeInCollection($collection, $criteria = [])
{
$collection = $this->driver->getDbh()->selectCollection($collection);
$res = $collection->count($criteria);
\PHPUnit\Framework\Assert::assertLessThan(1, $res);
}
/**
* Grabs a data from collection
*
* ``` php
* <?php
* $user = $I->grabFromCollection('users', array('name' => 'miles'));
* ```
*
* @param $collection
* @param array $criteria
* @return array
*/
public function grabFromCollection($collection, $criteria = [])
{
$collection = $this->driver->getDbh()->selectCollection($collection);
return $collection->findOne($criteria);
}
/**
* Grabs the documents count from a collection
*
* ``` php
* <?php
* $count = $I->grabCollectionCount('users');
* // or
* $count = $I->grabCollectionCount('users', array('isAdmin' => true));
* ```
*
* @param $collection
* @param array $criteria
* @return integer
*/
public function grabCollectionCount($collection, $criteria = [])
{
$collection = $this->driver->getDbh()->selectCollection($collection);
return $collection->count($criteria);
}
/**
* Asserts that an element in a collection exists and is an Array
*
* ``` php
* <?php
* $I->seeElementIsArray('users', array('name' => 'John Doe') , 'data.skills');
* ```
*
* @param String $collection
* @param Array $criteria
* @param String $elementToCheck
*/
public function seeElementIsArray($collection, $criteria = [], $elementToCheck = null)
{
$collection = $this->driver->getDbh()->selectCollection($collection);
$res = $collection->count(
array_merge(
$criteria,
[
$elementToCheck => ['$exists' => true],
'$where' => "Array.isArray(this.{$elementToCheck})"
]
)
);
if ($res > 1) {
throw new \PHPUnit\Framework\ExpectationFailedException(
'Error: you should test against a single element criteria when asserting that elementIsArray'
);
}
\PHPUnit\Framework\Assert::assertEquals(1, $res, 'Specified element is not a Mongo Object');
}
/**
* Asserts that an element in a collection exists and is an Object
*
* ``` php
* <?php
* $I->seeElementIsObject('users', array('name' => 'John Doe') , 'data');
* ```
*
* @param String $collection
* @param Array $criteria
* @param String $elementToCheck
*/
public function seeElementIsObject($collection, $criteria = [], $elementToCheck = null)
{
$collection = $this->driver->getDbh()->selectCollection($collection);
$res = $collection->count(
array_merge(
$criteria,
[
$elementToCheck => ['$exists' => true],
'$where' => "! Array.isArray(this.{$elementToCheck}) && isObject(this.{$elementToCheck})"
]
)
);
if ($res > 1) {
throw new \PHPUnit\Framework\ExpectationFailedException(
'Error: you should test against a single element criteria when asserting that elementIsObject'
);
}
\PHPUnit\Framework\Assert::assertEquals(1, $res, 'Specified element is not a Mongo Object');
}
/**
* Count number of records in a collection
*
* ``` php
* <?php
* $I->seeNumElementsInCollection('users', 2);
* $I->seeNumElementsInCollection('users', 1, array('name' => 'miles'));
* ```
*
* @param $collection
* @param integer $expected
* @param array $criteria
*/
public function seeNumElementsInCollection($collection, $expected, $criteria = [])
{
$collection = $this->driver->getDbh()->selectCollection($collection);
$res = $collection->count($criteria);
\PHPUnit\Framework\Assert::assertSame($expected, $res);
}
/**
* Returns list of classes and corresponding packages required for this module
*/
public function _requires()
{
return ['MongoDB\Client' => '"mongodb/mongodb": "^1.0"'];
}
}

View File

@@ -0,0 +1,662 @@
<?php
namespace Codeception\Module;
use Phalcon\Di;
use PDOException;
use Phalcon\Mvc\Url;
use Phalcon\DiInterface;
use Phalcon\Di\Injectable;
use Codeception\TestInterface;
use Codeception\Configuration;
use Codeception\Lib\Framework;
use Phalcon\Mvc\RouterInterface;
use Phalcon\Mvc\Model as PhalconModel;
use Phalcon\Mvc\Router\RouteInterface;
use Codeception\Util\ReflectionHelper;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Interfaces\ActiveRecord;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Connector\Phalcon as PhalconConnector;
/**
* This module provides integration with [Phalcon framework](http://www.phalconphp.com/) (3.x).
* Please try it and leave your feedback.
*
* ## Demo Project
*
* <https://github.com/Codeception/phalcon-demo>
*
* ## Status
*
* * Maintainer: **Serghei Iakovlev**
* * Stability: **stable**
* * Contact: serghei@phalconphp.com
*
* ## Config
*
* The following configurations are required for this module:
*
* * bootstrap: `string`, default `app/config/bootstrap.php` - relative path to app.php config file
* * cleanup: `boolean`, default `true` - all database queries will be run in a transaction,
* which will be rolled back at the end of each test
* * savepoints: `boolean`, default `true` - use savepoints to emulate nested transactions
*
* The application bootstrap file must return Application object but not call its handle() method.
*
* ## API
*
* * di - `Phalcon\Di\Injectable` instance
* * client - `BrowserKit` client
*
* ## Parts
*
* By default all available methods are loaded, but you can specify parts to select only needed
* actions and avoid conflicts.
*
* * `orm` - include only `haveRecord/grabRecord/seeRecord/dontSeeRecord` actions.
* * `services` - allows to use `grabServiceFromContainer` and `addServiceToContainer`.
*
* Usage example:
*
* Sample bootstrap (`app/config/bootstrap.php`):
*
* ``` php
* <?php
* $config = include __DIR__ . "/config.php";
* include __DIR__ . "/loader.php";
* $di = new \Phalcon\DI\FactoryDefault();
* include __DIR__ . "/services.php";
* return new \Phalcon\Mvc\Application($di);
* ?>
* ```
*
* ```yaml
* actor: AcceptanceTester
* modules:
* enabled:
* - Phalcon:
* part: services
* bootstrap: 'app/config/bootstrap.php'
* cleanup: true
* savepoints: true
* - WebDriver:
* url: http://your-url.com
* browser: phantomjs
* ```
*/
class Phalcon extends Framework implements ActiveRecord, PartedModule
{
protected $config = [
'bootstrap' => 'app/config/bootstrap.php',
'cleanup' => true,
'savepoints' => true,
];
/**
* Phalcon bootstrap file path
*/
protected $bootstrapFile = null;
/**
* Dependency injection container
* @var DiInterface
*/
public $di = null;
/**
* Phalcon Connector
* @var PhalconConnector
*/
public $client;
/**
* HOOK: used after configuration is loaded
*
* @throws ModuleConfigException
*/
public function _initialize()
{
$this->bootstrapFile = Configuration::projectDir() . $this->config['bootstrap'];
if (!file_exists($this->bootstrapFile)) {
throw new ModuleConfigException(
__CLASS__,
"Bootstrap file does not exist in " . $this->config['bootstrap'] . "\n"
. "Please create the bootstrap file that returns Application object\n"
. "And specify path to it with 'bootstrap' config\n\n"
. "Sample bootstrap: \n\n<?php\n"
. '$config = include __DIR__ . "/config.php";' . "\n"
. 'include __DIR__ . "/loader.php";' . "\n"
. '$di = new \Phalcon\DI\FactoryDefault();' . "\n"
. 'include __DIR__ . "/services.php";' . "\n"
. 'return new \Phalcon\Mvc\Application($di);'
);
}
$this->client = new PhalconConnector();
}
/**
* HOOK: before scenario
*
* @param TestInterface $test
* @throws ModuleException
*/
public function _before(TestInterface $test)
{
/** @noinspection PhpIncludeInspection */
$application = require $this->bootstrapFile;
if (!$application instanceof Injectable) {
throw new ModuleException(__CLASS__, 'Bootstrap must return \Phalcon\Di\Injectable object');
}
$this->di = $application->getDI();
Di::reset();
Di::setDefault($this->di);
if ($this->di->has('session')) {
// Destroy existing sessions of previous tests
$this->di['session'] = new PhalconConnector\MemorySession();
}
if ($this->di->has('cookies')) {
$this->di['cookies']->useEncryption(false);
}
if ($this->config['cleanup'] && $this->di->has('db')) {
if ($this->config['savepoints']) {
$this->di['db']->setNestedTransactionsWithSavepoints(true);
}
$this->di['db']->begin();
$this->debugSection('Database', 'Transaction started');
}
// localize
$bootstrap = $this->bootstrapFile;
$this->client->setApplication(function () use ($bootstrap) {
$currentDi = Di::getDefault();
/** @noinspection PhpIncludeInspection */
$application = require $bootstrap;
$di = $application->getDI();
if ($currentDi->has('db')) {
$di['db'] = $currentDi['db'];
}
if ($currentDi->has('session')) {
$di['session'] = $currentDi['session'];
}
if ($di->has('cookies')) {
$di['cookies']->useEncryption(false);
}
return $application;
});
}
/**
* HOOK: after scenario
*
* @param TestInterface $test
*/
public function _after(TestInterface $test)
{
if ($this->config['cleanup'] && isset($this->di['db'])) {
while ($this->di['db']->isUnderTransaction()) {
$level = $this->di['db']->getTransactionLevel();
try {
$this->di['db']->rollback(true);
$this->debugSection('Database', 'Transaction cancelled; all changes reverted.');
} catch (PDOException $e) {
}
if ($level == $this->di['db']->getTransactionLevel()) {
break;
}
}
$this->di['db']->close();
}
$this->di = null;
Di::reset();
$_SESSION = $_FILES = $_GET = $_POST = $_COOKIE = $_REQUEST = [];
}
public function _parts()
{
return ['orm', 'services'];
}
/**
* Provides access the Phalcon application object.
*
* @see \Codeception\Lib\Connector\Phalcon::getApplication
* @return \Phalcon\Application|\Phalcon\Mvc\Micro
*/
public function getApplication()
{
return $this->client->getApplication();
}
/**
* Sets value to session. Use for authorization.
*
* @param string $key
* @param mixed $val
*/
public function haveInSession($key, $val)
{
$this->di->get('session')->set($key, $val);
$this->debugSection('Session', json_encode($this->di['session']->toArray()));
}
/**
* Checks that session contains value.
* If value is `null` checks that session has key.
*
* ``` php
* <?php
* $I->seeInSession('key');
* $I->seeInSession('key', 'value');
* ?>
* ```
*
* @param string $key
* @param mixed $value
*/
public function seeInSession($key, $value = null)
{
$this->debugSection('Session', json_encode($this->di['session']->toArray()));
if (is_array($key)) {
$this->seeSessionHasValues($key);
return;
}
if (!$this->di['session']->has($key)) {
$this->fail("No session variable with key '$key'");
}
if (is_null($value)) {
$this->assertTrue($this->di['session']->has($key));
} else {
$this->assertEquals($value, $this->di['session']->get($key));
}
}
/**
* Assert that the session has a given list of values.
*
* ``` php
* <?php
* $I->seeSessionHasValues(['key1', 'key2']);
* $I->seeSessionHasValues(['key1' => 'value1', 'key2' => 'value2']);
* ?>
* ```
*
* @param array $bindings
* @return void
*/
public function seeSessionHasValues(array $bindings)
{
foreach ($bindings as $key => $value) {
if (is_int($key)) {
$this->seeInSession($value);
} else {
$this->seeInSession($key, $value);
}
}
}
/**
* Inserts record into the database.
*
* ``` php
* <?php
* $user_id = $I->haveRecord('App\Models\Users', ['name' => 'Phalcon']);
* $I->haveRecord('App\Models\Categories', ['name' => 'Testing']');
* ?>
* ```
*
* @param string $model Model name
* @param array $attributes Model attributes
* @return mixed
* @part orm
*/
public function haveRecord($model, $attributes = [])
{
$record = $this->getModelRecord($model);
$res = $record->save($attributes);
$field = function ($field) {
if (is_array($field)) {
return implode(', ', $field);
}
return $field;
};
if (!$res) {
$messages = $record->getMessages();
$errors = [];
foreach ($messages as $message) {
/** @var \Phalcon\Mvc\Model\MessageInterface $message */
$errors[] = sprintf(
'[%s] %s: %s',
$message->getType(),
$field($message->getField()),
$message->getMessage()
);
}
$this->fail(sprintf("Record %s was not saved. Messages: \n%s", $model, implode(PHP_EOL, $errors)));
return null;
}
$this->debugSection($model, json_encode($record));
return $this->getModelIdentity($record);
}
/**
* Checks that record exists in database.
*
* ``` php
* <?php
* $I->seeRecord('App\Models\Categories', ['name' => 'Testing']);
* ?>
* ```
*
* @param string $model Model name
* @param array $attributes Model attributes
* @part orm
*/
public function seeRecord($model, $attributes = [])
{
$record = $this->findRecord($model, $attributes);
if (!$record) {
$this->fail("Couldn't find $model with " . json_encode($attributes));
}
$this->debugSection($model, json_encode($record));
}
/**
* Checks that record does not exist in database.
*
* ``` php
* <?php
* $I->dontSeeRecord('App\Models\Categories', ['name' => 'Testing']);
* ?>
* ```
*
* @param string $model Model name
* @param array $attributes Model attributes
* @part orm
*/
public function dontSeeRecord($model, $attributes = [])
{
$record = $this->findRecord($model, $attributes);
$this->debugSection($model, json_encode($record));
if ($record) {
$this->fail("Unexpectedly managed to find $model with " . json_encode($attributes));
}
}
/**
* Retrieves record from database
*
* ``` php
* <?php
* $category = $I->grabRecord('App\Models\Categories', ['name' => 'Testing']);
* ?>
* ```
*
* @param string $model Model name
* @param array $attributes Model attributes
* @return mixed
* @part orm
*/
public function grabRecord($model, $attributes = [])
{
return $this->findRecord($model, $attributes);
}
/**
* Resolves the service based on its configuration from Phalcon's DI container
* Recommended to use for unit testing.
*
* @param string $service Service name
* @param array $parameters Parameters [Optional]
* @return mixed
* @part services
*/
public function grabServiceFromContainer($service, array $parameters = [])
{
if (!$this->di->has($service)) {
$this->fail("Service $service is not available in container");
}
return $this->di->get($service, $parameters);
}
/**
* Alias for `grabServiceFromContainer`.
*
* Note: Deprecated. Will be removed in Codeception 2.3.
*
* @param string $service Service name
* @param array $parameters Parameters [Optional]
* @return mixed
* @part services
*/
public function grabServiceFromDi($service, array $parameters = [])
{
return $this->grabServiceFromContainer($service, $parameters);
}
/**
* Registers a service in the services container and resolve it. This record will be erased after the test.
* Recommended to use for unit testing.
*
* ``` php
* <?php
* $filter = $I->addServiceToContainer('filter', ['className' => '\Phalcon\Filter']);
* $filter = $I->addServiceToContainer('answer', function () {
* return rand(0, 1) ? 'Yes' : 'No';
* }, true);
* ?>
* ```
*
* @param string $name
* @param mixed $definition
* @param boolean $shared
* @return mixed|null
* @part services
*/
public function addServiceToContainer($name, $definition, $shared = false)
{
try {
$service = $this->di->set($name, $definition, $shared);
return $service->resolve();
} catch (\Exception $e) {
$this->fail($e->getMessage());
return null;
}
}
/**
* Alias for `addServiceToContainer`.
*
* Note: Deprecated. Will be removed in Codeception 2.3.
*
* @param string $name
* @param mixed $definition
* @param boolean $shared
* @return mixed|null
* @part services
*/
public function haveServiceInDi($name, $definition, $shared = false)
{
return $this->addServiceToContainer($name, $definition, $shared);
}
/**
* Opens web page using route name and parameters.
*
* ``` php
* <?php
* $I->amOnRoute('posts.create');
* ?>
* ```
*
* @param string $routeName
* @param array $params
*/
public function amOnRoute($routeName, array $params = [])
{
if (!$this->di->has('url')) {
$this->fail('Unable to resolve "url" service.');
}
/** @var Url $url */
$url = $this->di->getShared('url');
$urlParams = ['for' => $routeName];
if ($params) {
$urlParams += $params;
}
$this->amOnPage($url->get($urlParams, null, true));
}
/**
* Checks that current url matches route
*
* ``` php
* <?php
* $I->seeCurrentRouteIs('posts.index');
* ?>
* ```
* @param string $routeName
*/
public function seeCurrentRouteIs($routeName)
{
if (!$this->di->has('url')) {
$this->fail('Unable to resolve "url" service.');
}
/** @var Url $url */
$url = $this->di->getShared('url');
$this->seeCurrentUrlEquals($url->get(['for' => $routeName], null, true));
}
/**
* Allows to query the first record that match the specified conditions
*
* @param string $model Model name
* @param array $attributes Model attributes
*
* @return \Phalcon\Mvc\Model
*/
protected function findRecord($model, $attributes = [])
{
$this->getModelRecord($model);
$query = [];
foreach ($attributes as $key => $value) {
$query[] = "$key = '$value'";
}
$squery = implode(' AND ', $query);
$this->debugSection('Query', $squery);
return call_user_func_array([$model, 'findFirst'], [$squery]);
}
/**
* Get Model Record
*
* @param $model
*
* @return \Phalcon\Mvc\Model
* @throws ModuleException
*/
protected function getModelRecord($model)
{
if (!class_exists($model)) {
throw new ModuleException(__CLASS__, "Model $model does not exist");
}
$record = new $model;
if (!$record instanceof PhalconModel) {
throw new ModuleException(__CLASS__, "Model $model is not instance of \\Phalcon\\Mvc\\Model");
}
return $record;
}
/**
* Get identity.
*
* @param \Phalcon\Mvc\Model $model
* @return mixed
*/
protected function getModelIdentity(PhalconModel $model)
{
if (property_exists($model, 'id')) {
return $model->id;
}
if (!$this->di->has('modelsMetadata')) {
return null;
}
$primaryKeys = $this->di->get('modelsMetadata')->getPrimaryKeyAttributes($model);
switch (count($primaryKeys)) {
case 0:
return null;
case 1:
return $model->{$primaryKeys[0]};
default:
return array_intersect_key(get_object_vars($model), array_flip($primaryKeys));
}
}
/**
* Returns a list of recognized domain names
*
* @return array
*/
protected function getInternalDomains()
{
$internalDomains = [$this->getApplicationDomainRegex()];
/** @var RouterInterface $router */
$router = $this->di->get('router');
if ($router instanceof RouterInterface) {
/** @var RouteInterface[] $routes */
$routes = $router->getRoutes();
foreach ($routes as $route) {
if ($route instanceof RouteInterface) {
$hostName = $route->getHostname();
if (!empty($hostName)) {
$internalDomains[] = '/^' . str_replace('.', '\.', $route->getHostname()) . '$/';
}
}
}
}
return array_unique($internalDomains);
}
/**
* @return string
*/
private function getApplicationDomainRegex()
{
$server = ReflectionHelper::readPrivateProperty($this->client, 'server');
$domain = $server['HTTP_HOST'];
return '/^' . str_replace('.', '\.', $domain) . '$/';
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Connector\Guzzle6;
use Codeception\Lib\InnerBrowser;
use Codeception\Lib\Interfaces\MultiSession;
use Codeception\Lib\Interfaces\Remote;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\TestInterface;
use Codeception\Util\Uri;
use GuzzleHttp\Client as GuzzleClient;
/**
* Uses [Guzzle](http://guzzlephp.org/) to interact with your application over CURL.
* Module works over CURL and requires **PHP CURL extension** to be enabled.
*
* Use to perform web acceptance tests with non-javascript browser.
*
* If test fails stores last shown page in 'output' dir.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **stable**
* * Contact: codeception@codeception.com
*
*
* ## Configuration
*
* * url *required* - start url of your app
* * headers - default headers are set before each test.
* * handler (default: curl) - Guzzle handler to use. By default curl is used, also possible to pass `stream`, or any valid class name as [Handler](http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#handlers).
* * middleware - Guzzle middlewares to add. An array of valid callables is required.
* * curl - curl options
* * cookies - ...
* * auth - ...
* * verify - ...
* * .. those and other [Guzzle Request options](http://docs.guzzlephp.org/en/latest/request-options.html)
*
*
* ### Example (`acceptance.suite.yml`)
*
* modules:
* enabled:
* - PhpBrowser:
* url: 'http://localhost'
* auth: ['admin', '123345']
* curl:
* CURLOPT_RETURNTRANSFER: true
* cookies:
* cookie-1:
* Name: userName
* Value: john.doe
* cookie-2:
* Name: authToken
* Value: 1abcd2345
* Domain: subdomain.domain.com
* Path: /admin/
* Expires: 1292177455
* Secure: true
* HttpOnly: false
*
*
* All SSL certification checks are disabled by default.
* Use Guzzle request options to configure certifications and others.
*
* ## Public API
*
* Those properties and methods are expected to be used in Helper classes:
*
* Properties:
*
* * `guzzle` - contains [Guzzle](http://guzzlephp.org/) client instance: `\GuzzleHttp\Client`
* * `client` - Symfony BrowserKit instance.
*
*/
class PhpBrowser extends InnerBrowser implements Remote, MultiSession, RequiresPackage
{
private $isGuzzlePsr7;
protected $requiredFields = ['url'];
protected $config = [
'headers' => [],
'verify' => false,
'expect' => false,
'timeout' => 30,
'curl' => [],
'refresh_max_interval' => 10,
'handler' => 'curl',
'middleware' => null,
// required defaults (not recommended to change)
'allow_redirects' => false,
'http_errors' => false,
'cookies' => true,
];
protected $guzzleConfigFields = [
'auth',
'proxy',
'verify',
'cert',
'query',
'ssl_key',
'proxy',
'expect',
'version',
'timeout',
'connect_timeout'
];
/**
* @var \Codeception\Lib\Connector\Guzzle6
*/
public $client;
/**
* @var GuzzleClient
*/
public $guzzle;
public function _requires()
{
return ['GuzzleHttp\Client' => '"guzzlehttp/guzzle": ">=4.1.4 <7.0"'];
}
public function _initialize()
{
$this->_initializeSession();
}
protected function guessGuzzleConnector()
{
if (class_exists('GuzzleHttp\Url')) {
$this->isGuzzlePsr7 = false;
return new \Codeception\Lib\Connector\Guzzle();
}
$this->isGuzzlePsr7 = true;
return new \Codeception\Lib\Connector\Guzzle6();
}
public function _before(TestInterface $test)
{
if (!$this->client) {
$this->client = $this->guessGuzzleConnector();
}
$this->_prepareSession();
}
public function _getUrl()
{
return $this->config['url'];
}
/**
* Alias to `haveHttpHeader`
*
* @param $name
* @param $value
*/
public function setHeader($name, $value)
{
$this->haveHttpHeader($name, $value);
}
public function amHttpAuthenticated($username, $password)
{
$this->client->setAuth($username, $password);
}
public function amOnUrl($url)
{
$host = Uri::retrieveHost($url);
$this->_reconfigure(['url' => $host]);
$page = substr($url, strlen($host));
if ($page === '') {
$page = '/';
}
$this->debugSection('Host', $host);
$this->amOnPage($page);
}
public function amOnSubdomain($subdomain)
{
$url = $this->config['url'];
$url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain
$url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new
$this->_reconfigure(['url' => $url]);
}
protected function onReconfigure()
{
$this->_prepareSession();
}
/**
* Low-level API method.
* If Codeception commands are not enough, use [Guzzle HTTP Client](http://guzzlephp.org/) methods directly
*
* Example:
*
* ``` php
* <?php
* $I->executeInGuzzle(function (\GuzzleHttp\Client $client) {
* $client->get('/get', ['query' => ['foo' => 'bar']]);
* });
* ?>
* ```
*
* It is not recommended to use this command on a regular basis.
* If Codeception lacks important Guzzle Client methods, implement them and submit patches.
*
* @param callable $function
*/
public function executeInGuzzle(\Closure $function)
{
return $function($this->guzzle);
}
public function _getResponseCode()
{
return $this->getResponseStatusCode();
}
public function _initializeSession()
{
// independent sessions need independent cookies
$this->client = $this->guessGuzzleConnector();
$this->_prepareSession();
}
public function _prepareSession()
{
$defaults = array_intersect_key($this->config, array_flip($this->guzzleConfigFields));
$curlOptions = [];
foreach ($this->config['curl'] as $key => $val) {
if (defined($key)) {
$curlOptions[constant($key)] = $val;
}
}
$this->headers = $this->config['headers'];
$this->setCookiesFromOptions();
if ($this->isGuzzlePsr7) {
$defaults['base_uri'] = $this->config['url'];
$defaults['curl'] = $curlOptions;
$handler = Guzzle6::createHandler($this->config['handler']);
if ($handler && is_array($this->config['middleware'])) {
foreach ($this->config['middleware'] as $middleware) {
$handler->push($middleware);
}
}
$defaults['handler'] = $handler;
$this->guzzle = new GuzzleClient($defaults);
} else {
$defaults['config']['curl'] = $curlOptions;
$this->guzzle = new GuzzleClient(['base_url' => $this->config['url'], 'defaults' => $defaults]);
$this->client->setBaseUri($this->config['url']);
}
$this->client->setRefreshMaxInterval($this->config['refresh_max_interval']);
$this->client->setClient($this->guzzle);
}
public function _backupSession()
{
return [
'client' => $this->client,
'guzzle' => $this->guzzle,
'crawler' => $this->crawler,
'headers' => $this->headers,
];
}
public function _loadSession($session)
{
foreach ($session as $key => $val) {
$this->$key = $val;
}
}
public function _closeSession($session = null)
{
unset($session);
}
}

View File

@@ -0,0 +1,402 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Driver\AmazonSQS;
use Codeception\Lib\Driver\Beanstalk;
use Codeception\Lib\Driver\Iron;
/**
*
* Works with Queue servers.
*
* Testing with a selection of remote/local queueing services, including Amazon's SQS service
* Iron.io service and beanstalkd service.
*
* Supported and tested queue types are:
*
* * [Iron.io](http://iron.io/)
* * [Beanstalkd](http://kr.github.io/beanstalkd/)
* * [Amazon SQS](http://aws.amazon.com/sqs/)
*
* The following dependencies are needed for the listed queue servers:
*
* * Beanstalkd: pda/pheanstalk ~3.0
* * Amazon SQS: aws/aws-sdk-php
* * IronMQ: iron-io/iron_mq
*
* ## Status
*
* * Maintainer: **nathanmac**
* * Stability:
* - Iron.io: **stable**
* - Beanstalkd: **stable**
* - Amazon SQS: **stable**
* * Contact: nathan.macnamara@outlook.com
*
* ## Config
*
* The configuration settings depending on which queueing service is being used, all the options are listed
* here. Refer to the configuration examples below to identify the configuration options required for your chosen
* service.
*
* * type - type of queueing server (defaults to beanstalkd).
* * host - hostname/ip address of the queue server or the host for the iron.io when using iron.io service.
* * port: 11300 - port number for the queue server.
* * timeout: 90 - timeout settings for connecting the queue server.
* * token - Iron.io access token.
* * project - Iron.io project ID.
* * key - AWS access key ID.
* * version - AWS version (e.g. latest)
* * endpoint - The full URI of the webservice. This is only required when connecting to a custom endpoint (e.g., a local version of SQS).
* * secret - AWS secret access key.
* Warning:
* Hard-coding your credentials can be dangerous, because it is easy to accidentally commit your credentials
* into an SCM repository, potentially exposing your credentials to more people than intended.
* It can also make it difficult to rotate credentials in the future.
* * profile - AWS credential profile
* - it should be located in ~/.aws/credentials file
* - eg: [default]
* aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID
* aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY
* [project1]
* aws_access_key_id = YOUR_AWS_ACCESS_KEY_ID
* aws_secret_access_key = YOUR_AWS_SECRET_ACCESS_KEY
* - Note: Using IAM roles is the preferred technique for providing credentials
* to applications running on Amazon EC2
* http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html?highlight=credentials
*
* * region - A region parameter is also required for AWS, refer to the AWS documentation for possible values list.
*
* ### Example
* #### Example (beanstalkd)
*
* modules:
* enabled: [Queue]
* config:
* Queue:
* type: 'beanstalkd'
* host: '127.0.0.1'
* port: 11300
* timeout: 120
*
* #### Example (Iron.io)
*
* modules:
* enabled: [Queue]
* config:
* Queue:
* 'type': 'iron',
* 'host': 'mq-aws-us-east-1.iron.io',
* 'token': 'your-token',
* 'project': 'your-project-id'
*
* #### Example (AWS SQS)
*
* modules:
* enabled: [Queue]
* config:
* Queue:
* 'type': 'aws',
* 'key': 'your-public-key',
* 'secret': 'your-secret-key',
* 'region': 'us-west-2'
*
* #### Example AWS SQS using profile credentials
*
* modules:
* enabled: [Queue]
* config:
* Queue:
* 'type': 'aws',
* 'profile': 'project1', //see documentation
* 'region': 'us-west-2'
*
* #### Example AWS SQS running on Amazon EC2 instance
*
* modules:
* enabled: [Queue]
* config:
* Queue:
* 'type': 'aws',
* 'region': 'us-west-2'
*
*/
class Queue extends CodeceptionModule
{
/**
* @var \Codeception\Lib\Interfaces\Queue
*/
public $queueDriver;
/**
* Setup connection and open/setup the connection with config settings
*
* @param \Codeception\TestInterface $test
*/
public function _before(TestInterface $test)
{
$this->queueDriver->openConnection($this->config);
}
/**
* Provide and override for the config settings and allow custom settings depending on the service being used.
*/
protected function validateConfig()
{
$this->queueDriver = $this->createQueueDriver();
$this->requiredFields = $this->queueDriver->getRequiredConfig();
$this->config = array_merge($this->queueDriver->getDefaultConfig(), $this->config);
parent::validateConfig();
}
/**
* @return \Codeception\Lib\Interfaces\Queue
* @throws ModuleConfigException
*/
protected function createQueueDriver()
{
switch ($this->config['type']) {
case 'aws':
case 'sqs':
case 'aws_sqs':
return new AmazonSQS();
case 'iron':
case 'iron_mq':
return new Iron();
case 'beanstalk':
case 'beanstalkd':
case 'beanstalkq':
return new Beanstalk();
default:
throw new ModuleConfigException(
__CLASS__,
"Unknown queue type {$this->config}; Supported queue types are: aws, iron, beanstalk"
);
}
}
// ----------- SEARCH METHODS BELOW HERE ------------------------//
/**
* Check if a queue/tube exists on the queueing server.
*
* ```php
* <?php
* $I->seeQueueExists('default');
* ?>
* ```
*
* @param string $queue Queue Name
*/
public function seeQueueExists($queue)
{
$this->assertContains($queue, $this->queueDriver->getQueues());
}
/**
* Check if a queue/tube does NOT exist on the queueing server.
*
* ```php
* <?php
* $I->dontSeeQueueExists('default');
* ?>
* ```
*
* @param string $queue Queue Name
*/
public function dontSeeQueueExists($queue)
{
$this->assertNotContains($queue, $this->queueDriver->getQueues());
}
/**
* Check if a queue/tube is empty of all messages
*
* ```php
* <?php
* $I->seeEmptyQueue('default');
* ?>
* ```
*
* @param string $queue Queue Name
*/
public function seeEmptyQueue($queue)
{
$this->assertEquals(0, $this->queueDriver->getMessagesCurrentCountOnQueue($queue));
}
/**
* Check if a queue/tube is NOT empty of all messages
*
* ```php
* <?php
* $I->dontSeeEmptyQueue('default');
* ?>
* ```
*
* @param string $queue Queue Name
*/
public function dontSeeEmptyQueue($queue)
{
$this->assertNotEquals(0, $this->queueDriver->getMessagesCurrentCountOnQueue($queue));
}
/**
* Check if a queue/tube has a given current number of messages
*
* ```php
* <?php
* $I->seeQueueHasCurrentCount('default', 10);
* ?>
* ```
*
* @param string $queue Queue Name
* @param int $expected Number of messages expected
*/
public function seeQueueHasCurrentCount($queue, $expected)
{
$this->assertEquals($expected, $this->queueDriver->getMessagesCurrentCountOnQueue($queue));
}
/**
* Check if a queue/tube does NOT have a given current number of messages
*
* ```php
* <?php
* $I->dontSeeQueueHasCurrentCount('default', 10);
* ?>
* ```
*
* @param string $queue Queue Name
* @param int $expected Number of messages expected
*/
public function dontSeeQueueHasCurrentCount($queue, $expected)
{
$this->assertNotEquals($expected, $this->queueDriver->getMessagesCurrentCountOnQueue($queue));
}
/**
* Check if a queue/tube has a given total number of messages
*
* ```php
* <?php
* $I->seeQueueHasTotalCount('default', 10);
* ?>
* ```
*
* @param string $queue Queue Name
* @param int $expected Number of messages expected
*/
public function seeQueueHasTotalCount($queue, $expected)
{
$this->assertEquals($expected, $this->queueDriver->getMessagesTotalCountOnQueue($queue));
}
/**
* Check if a queue/tube does NOT have a given total number of messages
*
* ```php
* <?php
* $I->dontSeeQueueHasTotalCount('default', 10);
* ?>
* ```
*
* @param string $queue Queue Name
* @param int $expected Number of messages expected
*/
public function dontSeeQueueHasTotalCount($queue, $expected)
{
$this->assertNotEquals($expected, $this->queueDriver->getMessagesTotalCountOnQueue($queue));
}
// ----------- UTILITY METHODS BELOW HERE -------------------------//
/**
* Add a message to a queue/tube
*
* ```php
* <?php
* $I->addMessageToQueue('this is a messages', 'default');
* ?>
* ```
*
* @param string $message Message Body
* @param string $queue Queue Name
*/
public function addMessageToQueue($message, $queue)
{
$this->queueDriver->addMessageToQueue($message, $queue);
}
/**
* Clear all messages of the queue/tube
*
* ```php
* <?php
* $I->clearQueue('default');
* ?>
* ```
*
* @param string $queue Queue Name
*/
public function clearQueue($queue)
{
$this->queueDriver->clearQueue($queue);
}
// ----------- GRABBER METHODS BELOW HERE -----------------------//
/**
* Grabber method to get the list of queues/tubes on the server
*
* ```php
* <?php
* $queues = $I->grabQueues();
* ?>
* ```
*
* @return array List of Queues/Tubes
*/
public function grabQueues()
{
return $this->queueDriver->getQueues();
}
/**
* Grabber method to get the current number of messages on the queue/tube (pending/ready)
*
* ```php
* <?php
* $I->grabQueueCurrentCount('default');
* ?>
* ```
* @param string $queue Queue Name
*
* @return int Count
*/
public function grabQueueCurrentCount($queue)
{
return $this->queueDriver->getMessagesCurrentCountOnQueue($queue);
}
/**
* Grabber method to get the total number of messages on the queue/tube
*
* ```php
* <?php
* $I->grabQueueTotalCount('default');
* ?>
* ```
*
* @param $queue Queue Name
*
* @return int Count
*/
public function grabQueueTotalCount($queue)
{
return $this->queueDriver->getMessagesTotalCountOnQueue($queue);
}
}

View File

@@ -0,0 +1,72 @@
# Modules
Modules are high-level extensions that are used in tests. Modules are created for each test suites (according to suite configuration) and can be accessed directly from unit tests:
```php
<?php
$this->getModule('PhpBrowser')->client;
?>
```
or used inside scenario-driven tests, where `$I` acts as an wrapper to different modules
```php
<?php
$I->click(); // => PhpBrowser
$I->seeInDatabase(); // => Db
?>
```
Each module is extending `Codeception\Module` class and defined in `Codeception\Module` namespace. All Codeception modules are autoloaded by searching in this particular namespace: `PhpBrowser` => `Codeception\Module\PhpBrowser`.
## What you should know before developing a module
The core principles:
1. Public methods of modules are actions of an actor inside a test. That's why they should be named in proper format:
```
doSomeStuff() => $I->doSomeStuff() => I do some stuff
doSomeStuffWith($a, $b) => $I->doSomeStuffWith("vodka", "gin"); => I do some stuff with "vodka", "gin"
seeIsGreat() => $I->seeIsGreat() => I see is great
```
* Each method that define environment should start with `am` or `have`
* Each assertion should start with `see` prefix
* Each method that returns values should start with `grab` (grabbers) or `have` (definitions)
Example:
```php
$I->amSeller();
$I->haveProducts(['vodka', 'gin']);
$I->haveDiscount('0.1');
$I->setPrice('gin', '10$');
$I->seePrice('gin', '9.9');
$price = $I->grabPriceFor('gin');
```
2. Configuration parameters are set in `.suite.yml` config and stored in `config` property array of a module. All default values can be set there as well. Required parameters should be set in `requiredFields` property.
```php
<?php
protected $config = ['browser' => 'firefox'];
protected $requiredFields = ['url'];
?>
```
You should not perform validation if `url` was set. Module would perform it for you, so you could access `$this->config['url']` inside a module.
3. If you use low-level clients in your module (PDO driver, framework client, selenium client) you should allow developers to access them. That's why you should define their instances as `public` properties of method.
Also you *may* provide a closure method to access low-level API
```php
<?php
$I->executeInSelenium(function(\WebDriverClient $wb) {
$wd->manage()->addCookie(['name' => 'verified']);
});
?>
```
4. Modules can be added to official repo, or published standalone. In any case module should be defined in `Codeception\Module` namespace. If you develop a module and you think it might be useful to others, please ask in Github Issues, maybe we would like to include it into the official repo.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,695 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\RequiresPackage;
use Codeception\Module as CodeceptionModule;
use Codeception\Exception\ModuleException;
use Codeception\TestInterface;
use Predis\Client as RedisDriver;
/**
* This module uses the [Predis](https://github.com/nrk/predis) library
* to interact with a Redis server.
*
* ## Status
*
* * Stability: **beta**
*
* ## Configuration
*
* * **`host`** (`string`, default `'127.0.0.1'`) - The Redis host
* * **`port`** (`int`, default `6379`) - The Redis port
* * **`database`** (`int`, no default) - The Redis database. Needs to be specified.
* * **`cleanupBefore`**: (`string`, default `'never'`) - Whether/when to flush the database:
* * `suite`: at the beginning of every suite
* * `test`: at the beginning of every test
* * Any other value: never
*
* ### Example (`unit.suite.yml`)
*
* ```yaml
* modules:
* - Redis:
* host: '127.0.0.1'
* port: 6379
* database: 0
* cleanupBefore: 'never'
* ```
*
* ## Public Properties
*
* * **driver** - Contains the Predis client/driver
*
* @author Marc Verney <marc@marcverney.net>
*/
class Redis extends CodeceptionModule implements RequiresPackage
{
/**
* {@inheritdoc}
*
* No default value is set for the database, using this parameter.
*/
protected $config = [
'host' => '127.0.0.1',
'port' => 6379,
'cleanupBefore' => 'never'
];
/**
* {@inheritdoc}
*/
protected $requiredFields = [
'database'
];
/**
* The Redis driver
*
* @var RedisDriver
*/
public $driver;
public function _requires()
{
return ['Predis\Client' => '"predis/predis": "^1.0"'];
}
/**
* Instructions to run after configuration is loaded
*
* @throws ModuleException
*/
public function _initialize()
{
try {
$this->driver = new RedisDriver([
'host' => $this->config['host'],
'port' => $this->config['port'],
'database' => $this->config['database']
]);
} catch (\Exception $e) {
throw new ModuleException(
__CLASS__,
$e->getMessage()
);
}
}
/**
* Code to run before each suite
*
* @param array $settings
*/
public function _beforeSuite($settings = [])
{
if ($this->config['cleanupBefore'] === 'suite') {
$this->cleanup();
}
}
/**
* Code to run before each test
*
* @param TestInterface $test
*/
public function _before(TestInterface $test)
{
if ($this->config['cleanupBefore'] === 'test') {
$this->cleanup();
}
}
/**
* Delete all the keys in the Redis database
*
* @throws ModuleException
*/
public function cleanup()
{
try {
$this->driver->flushdb();
} catch (\Exception $e) {
throw new ModuleException(
__CLASS__,
$e->getMessage()
);
}
}
/**
* Returns the value of a given key
*
* Examples:
*
* ``` php
* <?php
* // Strings
* $I->grabFromRedis('string');
*
* // Lists: get all members
* $I->grabFromRedis('example:list');
*
* // Lists: get a specific member
* $I->grabFromRedis('example:list', 2);
*
* // Lists: get a range of elements
* $I->grabFromRedis('example:list', 2, 4);
*
* // Sets: get all members
* $I->grabFromRedis('example:set');
*
* // ZSets: get all members
* $I->grabFromRedis('example:zset');
*
* // ZSets: get a range of members
* $I->grabFromRedis('example:zset', 3, 12);
*
* // Hashes: get all fields of a key
* $I->grabFromRedis('example:hash');
*
* // Hashes: get a specific field of a key
* $I->grabFromRedis('example:hash', 'foo');
* ```
*
* @param string $key The key name
*
* @return mixed
*
* @throws ModuleException if the key does not exist
*/
public function grabFromRedis($key)
{
$args = func_get_args();
switch ($this->driver->type($key)) {
case 'none':
throw new ModuleException(
$this,
"Cannot grab key \"$key\" as it does not exist"
);
break;
case 'string':
$reply = $this->driver->get($key);
break;
case 'list':
if (count($args) === 2) {
$reply = $this->driver->lindex($key, $args[1]);
} else {
$reply = $this->driver->lrange(
$key,
isset($args[1]) ? $args[1] : 0,
isset($args[2]) ? $args[2] : -1
);
}
break;
case 'set':
$reply = $this->driver->smembers($key);
break;
case 'zset':
if (count($args) === 2) {
throw new ModuleException(
$this,
"The method grabFromRedis(), when used with sorted "
. "sets, expects either one argument or three"
);
}
$reply = $this->driver->zrange(
$key,
isset($args[2]) ? $args[1] : 0,
isset($args[2]) ? $args[2] : -1,
'WITHSCORES'
);
break;
case 'hash':
$reply = isset($args[1])
? $this->driver->hget($key, $args[1])
: $this->driver->hgetall($key);
break;
default:
$reply = null;
}
return $reply;
}
/**
* Creates or modifies keys
*
* If $key already exists:
*
* - Strings: its value will be overwritten with $value
* - Other types: $value items will be appended to its value
*
* Examples:
*
* ``` php
* <?php
* // Strings: $value must be a scalar
* $I->haveInRedis('string', 'Obladi Oblada');
*
* // Lists: $value can be a scalar or an array
* $I->haveInRedis('list', ['riri', 'fifi', 'loulou']);
*
* // Sets: $value can be a scalar or an array
* $I->haveInRedis('set', ['riri', 'fifi', 'loulou']);
*
* // ZSets: $value must be an associative array with scores
* $I->haveInRedis('zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]);
*
* // Hashes: $value must be an associative array
* $I->haveInRedis('hash', ['obladi' => 'oblada']);
* ```
*
* @param string $type The type of the key
* @param string $key The key name
* @param mixed $value The value
*
* @throws ModuleException
*/
public function haveInRedis($type, $key, $value)
{
switch (strtolower($type)) {
case 'string':
if (!is_scalar($value)) {
throw new ModuleException(
$this,
'If second argument of haveInRedis() method is "string", '
. 'third argument must be a scalar'
);
}
$this->driver->set($key, $value);
break;
case 'list':
$this->driver->rpush($key, $value);
break;
case 'set':
$this->driver->sadd($key, $value);
break;
case 'zset':
if (!is_array($value)) {
throw new ModuleException(
$this,
'If second argument of haveInRedis() method is "zset", '
. 'third argument must be an (associative) array'
);
}
$this->driver->zadd($key, $value);
break;
case 'hash':
if (!is_array($value)) {
throw new ModuleException(
$this,
'If second argument of haveInRedis() method is "hash", '
. 'third argument must be an array'
);
}
$this->driver->hmset($key, $value);
break;
default:
throw new ModuleException(
$this,
"Unknown type \"$type\" for key \"$key\". Allowed types are "
. '"string", "list", "set", "zset", "hash"'
);
}
}
/**
* Asserts that a key does not exist or, optionally, that it doesn't have the
* provided $value
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key does not exist
* $I->dontSeeInRedis('example:string');
*
* // Checks a String does not exist or its value is not the one provided
* $I->dontSeeInRedis('example:string', 'life');
*
* // Checks a List does not exist or its value is not the one provided (order of elements is compared).
* $I->dontSeeInRedis('example:list', ['riri', 'fifi', 'loulou']);
*
* // Checks a Set does not exist or its value is not the one provided (order of members is ignored).
* $I->dontSeeInRedis('example:set', ['riri', 'fifi', 'loulou']);
*
* // Checks a ZSet does not exist or its value is not the one provided (scores are required, order of members is compared)
* $I->dontSeeInRedis('example:zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]);
*
* // Checks a Hash does not exist or its value is not the one provided (order of members is ignored).
* $I->dontSeeInRedis('example:hash', ['riri' => true, 'fifi' => 'Dewey', 'loulou' => 2]);
* ```
*
* @param string $key The key name
* @param mixed $value Optional. If specified, also checks the key has this
* value. Booleans will be converted to 1 and 0 (even inside arrays)
*/
public function dontSeeInRedis($key, $value = null)
{
$this->assertFalse(
(bool) $this->checkKeyExists($key, $value),
"The key \"$key\" exists" . ($value ? ' and its value matches the one provided' : '')
);
}
/**
* Asserts that a given key does not contain a given item
*
* Examples:
*
* ``` php
* <?php
* // Strings: performs a substring search
* $I->dontSeeRedisKeyContains('string', 'bar');
*
* // Lists
* $I->dontSeeRedisKeyContains('example:list', 'poney');
*
* // Sets
* $I->dontSeeRedisKeyContains('example:set', 'cat');
*
* // ZSets: check whether the zset has this member
* $I->dontSeeRedisKeyContains('example:zset', 'jordan');
*
* // ZSets: check whether the zset has this member with this score
* $I->dontSeeRedisKeyContains('example:zset', 'jordan', 23);
*
* // Hashes: check whether the hash has this field
* $I->dontSeeRedisKeyContains('example:hash', 'magic');
*
* // Hashes: check whether the hash has this field with this value
* $I->dontSeeRedisKeyContains('example:hash', 'magic', 32);
* ```
*
* @param string $key The key
* @param mixed $item The item
* @param null $itemValue Optional and only used for zsets and hashes. If
* specified, the method will also check that the $item has this value/score
*
* @return bool
*/
public function dontSeeRedisKeyContains($key, $item, $itemValue = null)
{
$this->assertFalse(
(bool) $this->checkKeyContains($key, $item, $itemValue),
"The key \"$key\" contains " . (
is_null($itemValue)
? "\"$item\""
: "[\"$item\" => \"$itemValue\"]"
)
);
}
/**
* Asserts that a key exists, and optionally that it has the provided $value
*
* Examples:
*
* ``` php
* <?php
* // With only one argument, only checks the key exists
* $I->seeInRedis('example:string');
*
* // Checks a String exists and has the value "life"
* $I->seeInRedis('example:string', 'life');
*
* // Checks the value of a List. Order of elements is compared.
* $I->seeInRedis('example:list', ['riri', 'fifi', 'loulou']);
*
* // Checks the value of a Set. Order of members is ignored.
* $I->seeInRedis('example:set', ['riri', 'fifi', 'loulou']);
*
* // Checks the value of a ZSet. Scores are required. Order of members is compared.
* $I->seeInRedis('example:zset', ['riri' => 1, 'fifi' => 2, 'loulou' => 3]);
*
* // Checks the value of a Hash. Order of members is ignored.
* $I->seeInRedis('example:hash', ['riri' => true, 'fifi' => 'Dewey', 'loulou' => 2]);
* ```
*
* @param string $key The key name
* @param mixed $value Optional. If specified, also checks the key has this
* value. Booleans will be converted to 1 and 0 (even inside arrays)
*/
public function seeInRedis($key, $value = null)
{
$this->assertTrue(
(bool) $this->checkKeyExists($key, $value),
"Cannot find key \"$key\"" . ($value ? ' with the provided value' : '')
);
}
/**
* Sends a command directly to the Redis driver. See documentation at
* https://github.com/nrk/predis
* Every argument that follows the $command name will be passed to it.
*
* Examples:
*
* ``` php
* <?php
* $I->sendCommandToRedis('incr', 'example:string');
* $I->sendCommandToRedis('strLen', 'example:string');
* $I->sendCommandToRedis('lPop', 'example:list');
* $I->sendCommandToRedis('zRangeByScore', 'example:set', '-inf', '+inf', ['withscores' => true, 'limit' => [1, 2]]);
* $I->sendCommandToRedis('flushdb');
* ```
*
* @param string $command The command name
*
* @return mixed
*/
public function sendCommandToRedis($command)
{
return call_user_func_array(
[$this->driver, $command],
array_slice(func_get_args(), 1)
);
}
/**
* Asserts that a given key contains a given item
*
* Examples:
*
* ``` php
* <?php
* // Strings: performs a substring search
* $I->seeRedisKeyContains('example:string', 'bar');
*
* // Lists
* $I->seeRedisKeyContains('example:list', 'poney');
*
* // Sets
* $I->seeRedisKeyContains('example:set', 'cat');
*
* // ZSets: check whether the zset has this member
* $I->seeRedisKeyContains('example:zset', 'jordan');
*
* // ZSets: check whether the zset has this member with this score
* $I->seeRedisKeyContains('example:zset', 'jordan', 23);
*
* // Hashes: check whether the hash has this field
* $I->seeRedisKeyContains('example:hash', 'magic');
*
* // Hashes: check whether the hash has this field with this value
* $I->seeRedisKeyContains('example:hash', 'magic', 32);
* ```
*
* @param string $key The key
* @param mixed $item The item
* @param null $itemValue Optional and only used for zsets and hashes. If
* specified, the method will also check that the $item has this value/score
*
* @return bool
*/
public function seeRedisKeyContains($key, $item, $itemValue = null)
{
$this->assertTrue(
(bool) $this->checkKeyContains($key, $item, $itemValue),
"The key \"$key\" does not contain " . (
is_null($itemValue)
? "\"$item\""
: "[\"$item\" => \"$itemValue\"]"
)
);
}
/**
* Converts boolean values to "0" and "1"
*
* @param mixed $var The variable
*
* @return mixed
*/
private function boolToString($var)
{
$copy = is_array($var) ? $var : [$var];
foreach ($copy as $key => $value) {
if (is_bool($value)) {
$copy[$key] = $value ? '1' : '0';
}
}
return is_array($var) ? $copy : $copy[0];
}
/**
* Checks whether a key contains a given item
*
* @param string $key The key
* @param mixed $item The item
* @param null $itemValue Optional and only used for zsets and hashes. If
* specified, the method will also check that the $item has this value/score
*
* @return bool
*
* @throws ModuleException
*/
private function checkKeyContains($key, $item, $itemValue = null)
{
$result = null;
if (!is_scalar($item)) {
throw new ModuleException(
$this,
"All arguments of [dont]seeRedisKeyContains() must be scalars"
);
}
switch ($this->driver->type($key)) {
case 'string':
$reply = $this->driver->get($key);
$result = strpos($reply, $item) !== false;
break;
case 'list':
$reply = $this->driver->lrange($key, 0, -1);
$result = in_array($item, $reply);
break;
case 'set':
$result = $this->driver->sismember($key, $item);
break;
case 'zset':
$reply = $this->driver->zscore($key, $item);
if (is_null($reply)) {
$result = false;
} elseif (!is_null($itemValue)) {
$result = (float) $reply === (float) $itemValue;
} else {
$result = true;
}
break;
case 'hash':
$reply = $this->driver->hget($key, $item);
$result = is_null($itemValue)
? !is_null($reply)
: (string) $reply === (string) $itemValue;
break;
case 'none':
throw new ModuleException(
$this,
"Key \"$key\" does not exist"
);
break;
}
return $result;
}
/**
* Checks whether a key exists and, optionally, whether it has a given $value
*
* @param string $key The key name
* @param mixed $value Optional. If specified, also checks the key has this
* value. Booleans will be converted to 1 and 0 (even inside arrays)
*
* @return bool
*/
private function checkKeyExists($key, $value = null)
{
$type = $this->driver->type($key);
if (is_null($value)) {
return $type != 'none';
}
$value = $this->boolToString($value);
switch ($type) {
case 'string':
$reply = $this->driver->get($key);
// Allow non strict equality (2 equals '2')
$result = $reply == $value;
break;
case 'list':
$reply = $this->driver->lrange($key, 0, -1);
// Check both arrays have the same key/value pairs + same order
$result = $reply === $value;
break;
case 'set':
$reply = $this->driver->smembers($key);
// Only check both arrays have the same values
sort($reply);
sort($value);
$result = $reply === $value;
break;
case 'zset':
$reply = $this->driver->zrange($key, 0, -1, 'WITHSCORES');
// Check both arrays have the same key/value pairs + same order
$reply = $this->scoresToFloat($reply);
$value = $this->scoresToFloat($value);
$result = $reply === $value;
break;
case 'hash':
$reply = $this->driver->hgetall($key);
// Only check both arrays have the same key/value pairs (==)
$result = $reply == $value;
break;
default:
$result = false;
}
return $result;
}
/**
* Explicitly cast the scores of a Zset associative array as float/double
*
* @param array $arr The ZSet associative array
*
* @return array
*/
private function scoresToFloat(array $arr)
{
foreach ($arr as $member => $score) {
$arr[$member] = (float) $score;
}
return $arr;
}
}

View File

@@ -0,0 +1,512 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\API;
use Codeception\Lib\Interfaces\DependsOnModule;
use Codeception\Lib\Notification;
use Codeception\Module as CodeceptionModule;
use Codeception\TestInterface;
use Codeception\Exception\ModuleException;
use Codeception\Exception\ModuleRequireException;
use Codeception\Lib\Framework;
use Codeception\Lib\InnerBrowser;
use Codeception\Util\Soap as SoapUtils;
use Codeception\Util\XmlStructure;
/**
* Module for testing SOAP WSDL web services.
* Send requests and check if response matches the pattern.
*
* This module can be used either with frameworks or PHPBrowser.
* It tries to guess the framework is is attached to.
* If a endpoint is a full url then it uses PHPBrowser.
*
* ### Using Inside Framework
*
* Please note, that PHP SoapServer::handle method sends additional headers.
* This may trigger warning: "Cannot modify header information"
* If you use PHP SoapServer with framework, try to block call to this method in testing environment.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **stable**
* * Contact: codecept@davert.mail.ua
*
* ## Configuration
*
* * endpoint *required* - soap wsdl endpoint
* * SOAPAction - replace SOAPAction HTTP header (Set to '' to SOAP 1.2)
*
* ## Public Properties
*
* * xmlRequest - last SOAP request (DOMDocument)
* * xmlResponse - last SOAP response (DOMDocument)
*
*/
class SOAP extends CodeceptionModule implements DependsOnModule
{
protected $config = [
'schema' => "",
'schema_url' => 'http://schemas.xmlsoap.org/soap/envelope/',
'framework_collect_buffer' => true
];
protected $requiredFields = ['endpoint'];
protected $dependencyMessage = <<<EOF
Example using PhpBrowser as backend for SOAP module.
--
modules:
enabled:
- SOAP:
depends: PhpBrowser
--
Framework modules can be used as well for functional testing of SOAP API.
EOF;
/**
* @var \Symfony\Component\BrowserKit\Client
*/
public $client = null;
public $isFunctional = false;
/**
* @var \DOMDocument
*/
public $xmlRequest = null;
/**
* @var \DOMDocument
*/
public $xmlResponse = null;
/**
* @var XmlStructure
*/
protected $xmlStructure = null;
/**
* @var InnerBrowser
*/
protected $connectionModule;
public function _before(TestInterface $test)
{
$this->client = &$this->connectionModule->client;
$this->buildRequest();
$this->xmlResponse = null;
$this->xmlStructure = null;
}
protected function onReconfigure()
{
$this->buildRequest();
$this->xmlResponse = null;
$this->xmlStructure = null;
}
public function _depends()
{
return ['Codeception\Lib\InnerBrowser' => $this->dependencyMessage];
}
public function _inject(InnerBrowser $connectionModule)
{
$this->connectionModule = $connectionModule;
if ($connectionModule instanceof Framework) {
$this->isFunctional = true;
}
}
private function getClient()
{
if (!$this->client) {
throw new ModuleRequireException($this, "Connection client is not available.");
}
return $this->client;
}
private function getXmlResponse()
{
if (!$this->xmlResponse) {
throw new ModuleException($this, "No XML response, use `\$I->sendSoapRequest` to receive it");
}
return $this->xmlResponse;
}
private function getXmlStructure()
{
if (!$this->xmlStructure) {
$this->xmlStructure = new XmlStructure($this->getXmlResponse());
}
return $this->xmlStructure;
}
/**
* Prepare SOAP header.
* Receives header name and parameters as array.
*
* Example:
*
* ``` php
* <?php
* $I->haveSoapHeader('AuthHeader', array('username' => 'davert', 'password' => '123345'));
* ```
*
* Will produce header:
*
* ```
* <soapenv:Header>
* <SessionHeader>
* <AuthHeader>
* <username>davert</username>
* <password>12345</password>
* </AuthHeader>
* </soapenv:Header>
* ```
*
* @param $header
* @param array $params
*/
public function haveSoapHeader($header, $params = [])
{
$soap_schema_url = $this->config['schema_url'];
$xml = $this->xmlRequest;
$xmlHeader = $xml->documentElement->getElementsByTagNameNS($soap_schema_url, 'Header')->item(0);
$headerEl = $xml->createElement($header);
SoapUtils::arrayToXml($xml, $headerEl, $params);
$xmlHeader->appendChild($headerEl);
}
/**
* Submits request to endpoint.
*
* Requires of api function name and parameters.
* Parameters can be passed either as DOMDocument, DOMNode, XML string, or array (if no attributes).
*
* You are allowed to execute as much requests as you need inside test.
*
* Example:
*
* ``` php
* $I->sendSoapRequest('UpdateUser', '<user><id>1</id><name>notdavert</name></user>');
* $I->sendSoapRequest('UpdateUser', \Codeception\Utils\Soap::request()->user
* ->id->val(1)->parent()
* ->name->val('notdavert');
* ```
*
* @param $request
* @param $body
*/
public function sendSoapRequest($action, $body = "")
{
$soap_schema_url = $this->config['schema_url'];
$xml = $this->xmlRequest;
$call = $xml->createElement('ns:' . $action);
if ($body) {
$bodyXml = SoapUtils::toXml($body);
if ($bodyXml->hasChildNodes()) {
foreach ($bodyXml->childNodes as $bodyChildNode) {
$bodyNode = $xml->importNode($bodyChildNode, true);
$call->appendChild($bodyNode);
}
}
}
$xmlBody = $xml->getElementsByTagNameNS($soap_schema_url, 'Body')->item(0);
// cleanup if body already set
foreach ($xmlBody->childNodes as $node) {
$xmlBody->removeChild($node);
}
$xmlBody->appendChild($call);
$this->debugSection("Request", $req = $xml->C14N());
if ($this->isFunctional && $this->config['framework_collect_buffer']) {
$response = $this->processInternalRequest($action, $req);
} else {
$response = $this->processExternalRequest($action, $req);
}
$this->debugSection("Response", (string) $response);
$this->xmlResponse = SoapUtils::toXml($response);
$this->xmlStructure = null;
}
/**
* Checks XML response equals provided XML.
* Comparison is done by canonicalizing both xml`s.
*
* Parameters can be passed either as DOMDocument, DOMNode, XML string, or array (if no attributes).
*
* Example:
*
* ``` php
* <?php
* $I->seeSoapResponseEquals("<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope><SOAP-ENV:Body><result>1</result></SOAP-ENV:Envelope>");
*
* $dom = new \DOMDocument();
* $dom->load($file);
* $I->seeSoapRequestIncludes($dom);
*
* ```
*
* @param $xml
*/
public function seeSoapResponseEquals($xml)
{
$xml = SoapUtils::toXml($xml);
$this->assertEquals($xml->C14N(), $this->getXmlResponse()->C14N());
}
/**
* Checks XML response includes provided XML.
* Comparison is done by canonicalizing both xml`s.
* Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes).
*
* Example:
*
* ``` php
* <?php
* $I->seeSoapResponseIncludes("<result>1</result>");
* $I->seeSoapRequestIncludes(\Codeception\Utils\Soap::response()->result->val(1));
*
* $dom = new \DDOMDocument();
* $dom->load('template.xml');
* $I->seeSoapRequestIncludes($dom);
* ?>
* ```
*
* @param $xml
*/
public function seeSoapResponseIncludes($xml)
{
$xml = $this->canonicalize($xml);
$this->assertContains($xml, $this->getXmlResponse()->C14N(), "found in XML Response");
}
/**
* Checks XML response equals provided XML.
* Comparison is done by canonicalizing both xml`s.
*
* Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes).
*
* @param $xml
*/
public function dontSeeSoapResponseEquals($xml)
{
$xml = SoapUtils::toXml($xml);
\PHPUnit\Framework\Assert::assertXmlStringNotEqualsXmlString($xml->C14N(), $this->getXmlResponse()->C14N());
}
/**
* Checks XML response does not include provided XML.
* Comparison is done by canonicalizing both xml`s.
* Parameter can be passed either as XmlBuilder, DOMDocument, DOMNode, XML string, or array (if no attributes).
*
* @param $xml
*/
public function dontSeeSoapResponseIncludes($xml)
{
$xml = $this->canonicalize($xml);
$this->assertNotContains($xml, $this->getXmlResponse()->C14N(), "found in XML Response");
}
/**
* Checks XML response contains provided structure.
* Response elements will be compared with XML provided.
* Only nodeNames are checked to see elements match.
*
* Example:
*
* ``` php
* <?php
*
* $I->seeSoapResponseContainsStructure("<query><name></name></query>");
* ?>
* ```
*
* Use this method to check XML of valid structure is returned.
* This method does not use schema for validation.
* This method does not require path from root to match the structure.
*
* @param $xml
*/
public function seeSoapResponseContainsStructure($xml)
{
$xml = SoapUtils::toXml($xml);
$this->debugSection("Structure", $xml->saveXML());
$this->assertTrue((bool)$this->getXmlStructure()->matchXmlStructure($xml), "this structure is in response");
}
/**
* Opposite to `seeSoapResponseContainsStructure`
* @param $xml
*/
public function dontSeeSoapResponseContainsStructure($xml)
{
$xml = SoapUtils::toXml($xml);
$this->debugSection("Structure", $xml->saveXML());
$this->assertFalse((bool)$this->getXmlStructure()->matchXmlStructure($xml), "this structure is in response");
}
/**
* Checks XML response with XPath locator
*
* ``` php
* <?php
* $I->seeSoapResponseContainsXPath('//root/user[@id=1]');
* ?>
* ```
*
* @param $xpath
*/
public function seeSoapResponseContainsXPath($xpath)
{
$this->assertTrue($this->getXmlStructure()->matchesXpath($xpath));
}
/**
* Checks XML response doesn't contain XPath locator
*
* ``` php
* <?php
* $I->dontSeeSoapResponseContainsXPath('//root/user[@id=1]');
* ?>
* ```
*
* @param $xpath
*/
public function dontSeeSoapResponseContainsXPath($xpath)
{
$this->assertFalse($this->getXmlStructure()->matchesXpath($xpath));
}
/**
* Checks response code from server.
*
* @param $code
*/
public function seeSoapResponseCodeIs($code)
{
$this->assertEquals(
$code,
$this->client->getInternalResponse()->getStatus(),
"soap response code matches expected"
);
}
/**
* @deprecated use seeSoapResponseCodeIs instead
*/
public function seeResponseCodeIs($code)
{
Notification::deprecate('SOAP::seeResponseCodeIs deprecated in favor of seeSoapResponseCodeIs', 'SOAP Module');
$this->seeSoapResponseCodeIs($code);
}
/**
* Finds and returns text contents of element.
* Element is matched by either CSS or XPath
*
* @version 1.1
* @param $cssOrXPath
* @return string
*/
public function grabTextContentFrom($cssOrXPath)
{
$el = $this->getXmlStructure()->matchElement($cssOrXPath);
return $el->textContent;
}
/**
* Finds and returns attribute of element.
* Element is matched by either CSS or XPath
*
* @version 1.1
* @param $cssOrXPath
* @param $attribute
* @return string
*/
public function grabAttributeFrom($cssOrXPath, $attribute)
{
$el = $this->getXmlStructure()->matchElement($cssOrXPath);
if (!$el->hasAttribute($attribute)) {
$this->fail("Attribute not found in element matched by '$cssOrXPath'");
}
return $el->getAttribute($attribute);
}
protected function getSchema()
{
return $this->config['schema'];
}
protected function canonicalize($xml)
{
return SoapUtils::toXml($xml)->C14N();
}
/**
* @return \DOMDocument
*/
protected function buildRequest()
{
$soap_schema_url = $this->config['schema_url'];
$xml = new \DOMDocument();
$root = $xml->createElement('soapenv:Envelope');
$xml->appendChild($root);
$root->setAttribute('xmlns:ns', $this->getSchema());
$root->setAttribute('xmlns:soapenv', $soap_schema_url);
$body = $xml->createElementNS($soap_schema_url, 'soapenv:Body');
$header = $xml->createElementNS($soap_schema_url, 'soapenv:Header');
$root->appendChild($header);
$root->appendChild($body);
$this->xmlRequest = $xml;
return $xml;
}
protected function processRequest($action, $body)
{
$this->getClient()->request(
'POST',
$this->config['endpoint'],
[],
[],
[
'HTTP_Content-Type' => 'text/xml; charset=UTF-8',
'HTTP_Content-Length' => strlen($body),
'HTTP_SOAPAction' => isset($this->config['SOAPAction']) ? $this->config['SOAPAction'] : $action
],
$body
);
}
protected function processInternalRequest($action, $body)
{
ob_start();
try {
$this->getClient()->setServerParameter('HTTP_HOST', 'localhost');
$this->processRequest($action, $body);
} catch (\ErrorException $e) {
// Zend_Soap outputs warning as an exception
if (strpos($e->getMessage(), 'Warning: Cannot modify header information') === false) {
ob_end_clean();
throw $e;
}
}
$response = ob_get_contents();
ob_end_clean();
return $response;
}
protected function processExternalRequest($action, $body)
{
$this->processRequest($action, $body);
return $this->client->getInternalResponse()->getContent();
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Codeception\Module;
use Codeception\Module as CodeceptionModule;
use Codeception\Exception\ModuleException;
use Codeception\TestInterface;
/**
* Sequence solves data cleanup issue in alternative way.
* Instead cleaning up the database between tests,
* you can use generated unique names, that should not conflict.
* When you create article on a site, for instance, you can assign it a unique name and then check it.
*
* This module has no actions, but introduces a function `sq` for generating unique sequences within test and
* `sqs` for generating unique sequences across suite.
*
* ### Usage
*
* Function `sq` generates sequence, the only parameter it takes, is id.
* You can get back to previously generated sequence using that id:
*
* ``` php
* <?php
* sq('post1'); // post1_521fbc63021eb
* sq('post2'); // post2_521fbc6302266
* sq('post1'); // post1_521fbc63021eb
* ```
*
* Example:
*
* ``` php
* <?php
* $I->wantTo('create article');
* $I->click('New Article');
* $I->fillField('Title', sq('Article'));
* $I->fillField('Body', 'Demo article with Lorem Ipsum');
* $I->click('save');
* $I->see(sq('Article') ,'#articles')
* ```
*
* Populating Database:
*
* ``` php
* <?php
*
* for ($i = 0; $i<10; $i++) {
* $I->haveInDatabase('users', array('login' => sq("user$i"), 'email' => sq("user$i").'@email.com');
* }
* ?>
* ```
*
* Cest Suite tests:
*
* ``` php
* <?php
* class UserTest
* {
* public function createUser(AcceptanceTester $I)
* {
* $I->createUser(sqs('user') . '@mailserver.com', sqs('login'), sqs('pwd'));
* }
*
* public function checkEmail(AcceptanceTester $I)
* {
* $I->seeInEmailTo(sqs('user') . '@mailserver.com', sqs('login'));
* }
*
* public function removeUser(AcceptanceTester $I)
* {
* $I->removeUser(sqs('user') . '@mailserver.com');
* }
* }
* ?>
* ```
*
* ### Config
*
* By default produces unique string with param as a prefix:
*
* ```
* sq('user') => 'user_876asd8as87a'
* ```
*
* This behavior can be configured using `prefix` config param.
*
* Old style sequences:
*
* ```yaml
* Sequence:
* prefix: '_'
* ```
*
* Using id param inside prefix:
*
* ```yaml
* Sequence:
* prefix: '{id}.'
* ```
*/
class Sequence extends CodeceptionModule
{
public static $hash = [];
public static $suiteHash = [];
public static $prefix = '';
protected $config = ['prefix' => '{id}_'];
public function _initialize()
{
static::$prefix = $this->config['prefix'];
}
public function _after(TestInterface $t)
{
self::$hash = [];
}
public function _afterSuite()
{
self::$suiteHash = [];
}
}
if (!function_exists('sq') && !function_exists('sqs')) {
require_once __DIR__ . '/../Util/sq.php';
} else {
throw new ModuleException('Codeception\Module\Sequence', "function 'sq' and 'sqs' already defined");
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Codeception\Module;
use Codeception\Configuration;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Framework;
use Codeception\Lib\Interfaces\DoctrineProvider;
use Codeception\TestInterface;
use Symfony\Component\HttpKernel\Client;
/**
* Module for testing Silex applications like you would regularly do with Silex\WebTestCase.
* This module uses Symfony2 Crawler and HttpKernel to emulate requests and get response.
*
* This module may be considered experimental and require feedback and pull requests from you.
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **alpha**
* * Contact: davert.codecept@resend.cc
*
* ## Config
*
* * app: **required** - path to Silex bootstrap file.
* * em_service: 'db.orm.em' - use the stated EntityManager to pair with Doctrine Module.
*
* ### Bootstrap File
*
* Bootstrap is the same as [WebTestCase.createApplication](http://silex.sensiolabs.org/doc/testing.html#webtestcase).
*
* ``` php
* <?php
* $app = require __DIR__.'/path/to/app.php';
* $app['debug'] = true;
* unset($app['exception_handler']);
*
* return $app; // optionally
* ?>
* ```
*
* ### Example (`functional.suite.yml`)
*
* modules:
* enabled:
* - Silex:
* app: 'app/bootstrap.php'
*
* ## Public Properties
*
* * app - `Silex\Application` instance received from bootstrap file
*
* Class Silex
* @package Codeception\Module
*/
class Silex extends Framework implements DoctrineProvider
{
/**
* @var \Silex\Application
*/
public $app;
protected $requiredFields = ['app'];
protected $config = [
'em_service' => 'db.orm.em'
];
public function _initialize()
{
if (!file_exists(Configuration::projectDir() . $this->config['app'])) {
throw new ModuleConfigException(__CLASS__, "Bootstrap file {$this->config['app']} not found");
}
$this->loadApp();
}
public function _before(TestInterface $test)
{
$this->loadApp();
$this->client = new Client($this->app);
}
public function _getEntityManager()
{
if (!isset($this->app[$this->config['em_service']])) {
return null;
}
return $this->app[$this->config['em_service']];
}
protected function loadApp()
{
$this->app = require Configuration::projectDir() . $this->config['app'];
// if $app is not returned but exists
if (isset($app)) {
$this->app = $app;
}
if (!isset($this->app)) {
throw new ModuleConfigException(__CLASS__, "\$app instance was not received from bootstrap file");
}
// make doctrine persistent
$db_orm_em = $this->_getEntityManager();
if ($db_orm_em) {
$this->app->extend($this->config['em_service'], function () use ($db_orm_em) {
return $db_orm_em;
});
}
// some Silex apps (like Bolt) may rely on global $app variable
$GLOBALS['app'] = $this->app;
}
/**
* Return an instance of a class from the Container.
*
* Example
* ``` php
* <?php
* $I->grabService('session');
* ?>
* ```
*
* @param string $service
* @return mixed
*/
public function grabService($service)
{
return $this->app[$service];
}
/**
* Returns a list of recognized domain names
*
* @return array
*/
public function getInternalDomains()
{
$internalDomains = [];
foreach ($this->app['routes'] as $route) {
if ($domain = $route->getHost()) {
$internalDomains[] = '/^' . preg_quote($domain, '/') . '$/';
}
}
return $internalDomains;
}
}

View File

@@ -0,0 +1,697 @@
<?php
namespace Codeception\Module;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Framework;
use Codeception\Exception\ModuleRequireException;
use Codeception\Lib\Connector\Symfony as SymfonyConnector;
use Codeception\Lib\Interfaces\DoctrineProvider;
use Codeception\Lib\Interfaces\PartedModule;
use Symfony\Component\Finder\Finder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* This module uses Symfony Crawler and HttpKernel to emulate requests and test response.
*
* ## Demo Project
*
* <https://github.com/Codeception/symfony-demo>
*
* ## Config
*
* ### Symfony 4.x
*
* * app_path: 'src' - in Symfony 4 Kernel is located inside `src`
* * environment: 'local' - environment used for load kernel
* * kernel_class: 'App\Kernel' - kernel class name
* * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
* * debug: true - turn on/off debug mode
* * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
* * rebootable_client: 'true' - reboot client's kernel before each request
*
* #### Example (`functional.suite.yml`) - Symfony 4 Directory Structure
*
* modules:
* enabled:
* - Symfony:
* app_path: 'src'
* environment: 'test'
*
*
* ### Symfony 3.x
*
* * app_path: 'app' - specify custom path to your app dir, where the kernel interface is located.
* * var_path: 'var' - specify custom path to your var dir, where bootstrap cache is located.
* * environment: 'local' - environment used for load kernel
* * kernel_class: 'AppKernel' - kernel class name
* * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
* * debug: true - turn on/off debug mode
* * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
* * rebootable_client: 'true' - reboot client's kernel before each request
*
* #### Example (`functional.suite.yml`) - Symfony 3 Directory Structure
*
* modules:
* enabled:
* - Symfony:
* app_path: 'app/front'
* var_path: 'var'
* environment: 'local_test'
*
*
* ### Symfony 2.x
*
* * app_path: 'app' - specify custom path to your app dir, where bootstrap cache and kernel interface is located.
* * environment: 'local' - environment used for load kernel
* * kernel_class: 'AppKernel' - kernel class name
* * debug: true - turn on/off debug mode
* * em_service: 'doctrine.orm.entity_manager' - use the stated EntityManager to pair with Doctrine Module.
* * cache_router: 'false' - enable router caching between tests in order to [increase performance](http://lakion.com/blog/how-did-we-speed-up-sylius-behat-suite-with-blackfire)
* * rebootable_client: 'true' - reboot client's kernel before each request
*
* ### Example (`functional.suite.yml`) - Symfony 2.x Directory Structure
*
* ```
* modules:
* - Symfony:
* app_path: 'app/front'
* environment: 'local_test'
* ```
*
* ## Public Properties
*
* * kernel - HttpKernel instance
* * client - current Crawler instance
*
* ## Parts
*
* * services - allows to use Symfony DIC only with WebDriver or PhpBrowser modules.
*
* Usage example:
*
* ```yaml
* actor: AcceptanceTester
* modules:
* enabled:
* - Symfony:
* part: SERVICES
* - Doctrine2:
* depends: Symfony
* - WebDriver:
* url: http://your-url.com
* browser: phantomjs
* ```
*
*/
class Symfony extends Framework implements DoctrineProvider, PartedModule
{
private static $possibleKernelClasses = [
'AppKernel', // Symfony Standard
'App\Kernel', // Symfony Flex
];
/**
* @var \Symfony\Component\HttpKernel\Kernel
*/
public $kernel;
public $config = [
'app_path' => 'app',
'var_path' => 'app',
'kernel_class' => null,
'environment' => 'test',
'debug' => true,
'cache_router' => false,
'em_service' => 'doctrine.orm.entity_manager',
'rebootable_client' => true,
];
/**
* @return array
*/
public function _parts()
{
return ['services'];
}
/**
* @var
*/
protected $kernelClass;
/**
* Services that should be persistent permanently for all tests
*
* @var array
*/
protected $permanentServices = [];
/**
* Services that should be persistent during test execution between kernel reboots
*
* @var array
*/
protected $persistentServices = [];
public function _initialize()
{
$this->initializeSymfonyCache();
$this->kernelClass = $this->getKernelClass();
$maxNestingLevel = 200; // Symfony may have very long nesting level
$xdebugMaxLevelKey = 'xdebug.max_nesting_level';
if (ini_get($xdebugMaxLevelKey) < $maxNestingLevel) {
ini_set($xdebugMaxLevelKey, $maxNestingLevel);
}
$this->kernel = new $this->kernelClass($this->config['environment'], $this->config['debug']);
$this->kernel->boot();
if ($this->config['cache_router'] === true) {
$this->persistService('router', true);
}
}
/**
* Require Symfonys bootstrap.php.cache only for PHP Version < 7
*
* @throws ModuleRequireException
*/
private function initializeSymfonyCache()
{
$cache = Configuration::projectDir() . $this->config['var_path'] . DIRECTORY_SEPARATOR . 'bootstrap.php.cache';
if (PHP_VERSION_ID < 70000 && !file_exists($cache)) {
throw new ModuleRequireException(
__CLASS__,
"Symfony bootstrap file not found in $cache\n \n" .
"Please specify path to bootstrap file using `var_path` config option\n \n" .
"If you are trying to load bootstrap from a Bundle provide path like:\n \n" .
"modules:\n enabled:\n" .
" - Symfony:\n" .
" var_path: '../../app'\n" .
" app_path: '../../app'"
);
}
if (file_exists($cache)) {
require_once $cache;
}
}
/**
* Initialize new client instance before each test
*/
public function _before(\Codeception\TestInterface $test)
{
$this->persistentServices = array_merge($this->persistentServices, $this->permanentServices);
$this->client = new SymfonyConnector($this->kernel, $this->persistentServices, $this->config['rebootable_client']);
}
/**
* Update permanent services after each test
*/
public function _after(\Codeception\TestInterface $test)
{
foreach ($this->permanentServices as $serviceName => $service) {
$this->permanentServices[$serviceName] = $this->grabService($serviceName);
}
parent::_after($test);
}
/**
* Retrieve Entity Manager.
*
* EM service is retrieved once and then that instance returned on each call
*/
public function _getEntityManager()
{
if ($this->kernel === null) {
$this->fail('Symfony2 platform module is not loaded');
}
if (!isset($this->permanentServices[$this->config['em_service']])) {
// try to persist configured EM
$this->persistService($this->config['em_service'], true);
if ($this->_getContainer()->has('doctrine')) {
$this->persistService('doctrine', true);
}
if ($this->_getContainer()->has('doctrine.orm.default_entity_manager')) {
$this->persistService('doctrine.orm.default_entity_manager', true);
}
if ($this->_getContainer()->has('doctrine.dbal.backend_connection')) {
$this->persistService('doctrine.dbal.backend_connection', true);
}
}
return $this->permanentServices[$this->config['em_service']];
}
/**
* Return container.
*
* @return ContainerInterface
*/
public function _getContainer()
{
return $this->kernel->getContainer();
}
/**
* Attempts to guess the kernel location.
*
* When the Kernel is located, the file is required.
*
* @return string The Kernel class name
*/
protected function getKernelClass()
{
$path = codecept_root_dir() . $this->config['app_path'];
if (!file_exists(codecept_root_dir() . $this->config['app_path'])) {
throw new ModuleRequireException(
__CLASS__,
"Can't load Kernel from $path.\n"
. "Directory does not exists. Use `app_path` parameter to provide valid application path"
);
}
$finder = new Finder();
$finder->name('*Kernel.php')->depth('0')->in($path);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new ModuleRequireException(
__CLASS__,
"File with Kernel class was not found at $path. "
. "Specify directory where file with Kernel class for your application is located with `app_path` parameter."
);
}
if (file_exists(codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php')) {
// ensure autoloader from this dir is loaded
require_once codecept_root_dir() . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
}
$filesRealPath = array_map(function ($file) {
require_once $file;
return $file->getRealPath();
}, $results);
$possibleKernelClasses = $this->getPossibleKernelClasses();
foreach ($possibleKernelClasses as $class) {
if (class_exists($class)) {
$refClass = new \ReflectionClass($class);
if ($file = array_search($refClass->getFileName(), $filesRealPath)) {
return $class;
}
}
}
throw new ModuleRequireException(
__CLASS__,
"Kernel class was not found in $file. "
. "Specify directory where file with Kernel class for your application is located with `app_path` parameter."
);
}
/**
* Get service $serviceName and add it to the lists of persistent services.
* If $isPermanent then service becomes persistent between tests
*
* @param string $serviceName
* @param boolean $isPermanent
*/
public function persistService($serviceName, $isPermanent = false)
{
$service = $this->grabService($serviceName);
$this->persistentServices[$serviceName] = $service;
if ($isPermanent) {
$this->permanentServices[$serviceName] = $service;
}
if ($this->client) {
$this->client->persistentServices[$serviceName] = $service;
}
}
/**
* Remove service $serviceName from the lists of persistent services.
*
* @param string $serviceName
*/
public function unpersistService($serviceName)
{
if (isset($this->persistentServices[$serviceName])) {
unset($this->persistentServices[$serviceName]);
}
if (isset($this->permanentServices[$serviceName])) {
unset($this->permanentServices[$serviceName]);
}
if ($this->client && isset($this->client->persistentServices[$serviceName])) {
unset($this->client->persistentServices[$serviceName]);
}
}
/**
* Invalidate previously cached routes.
*/
public function invalidateCachedRouter()
{
$this->unpersistService('router');
}
/**
* Opens web page using route name and parameters.
*
* ``` php
* <?php
* $I->amOnRoute('posts.create');
* $I->amOnRoute('posts.show', array('id' => 34));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function amOnRoute($routeName, array $params = [])
{
$router = $this->grabService('router');
if (!$router->getRouteCollection()->get($routeName)) {
$this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
}
$url = $router->generate($routeName, $params);
$this->amOnPage($url);
}
/**
* Checks that current url matches route.
*
* ``` php
* <?php
* $I->seeCurrentRouteIs('posts.index');
* $I->seeCurrentRouteIs('posts.show', array('id' => 8));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function seeCurrentRouteIs($routeName, array $params = [])
{
$router = $this->grabService('router');
if (!$router->getRouteCollection()->get($routeName)) {
$this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
try {
$match = $router->match($uri);
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
$this->fail(sprintf('The "%s" url does not match with any route', $uri));
}
$expected = array_merge(['_route' => $routeName], $params);
$intersection = array_intersect_assoc($expected, $match);
$this->assertEquals($expected, $intersection);
}
/**
* Checks that current url matches route.
* Unlike seeCurrentRouteIs, this can matches without exact route parameters
*
* ``` php
* <?php
* $I->seeCurrentRouteMatches('my_blog_pages');
* ?>
* ```
*
* @param $routeName
*/
public function seeInCurrentRoute($routeName)
{
$router = $this->grabService('router');
if (!$router->getRouteCollection()->get($routeName)) {
$this->fail(sprintf('Route with name "%s" does not exists.', $routeName));
}
$uri = explode('?', $this->grabFromCurrentUrl())[0];
try {
$matchedRouteName = $router->match($uri)['_route'];
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
$this->fail(sprintf('The "%s" url does not match with any route', $uri));
}
$this->assertEquals($matchedRouteName, $routeName);
}
/**
* Checks if the desired number of emails was sent.
* If no argument is provided then at least one email must be sent to satisfy the check.
*
* ``` php
* <?php
* $I->seeEmailIsSent(2);
* ?>
* ```
*
* @param null|int $expectedCount
*/
public function seeEmailIsSent($expectedCount = null)
{
$profile = $this->getProfile();
if (!$profile) {
$this->fail('Emails can\'t be tested without Profiler');
}
if (!$profile->hasCollector('swiftmailer')) {
$this->fail('Emails can\'t be tested without SwiftMailer connector');
}
if (!is_int($expectedCount) && !is_null($expectedCount)) {
$this->fail(sprintf(
'The required number of emails must be either an integer or null. "%s" was provided.',
print_r($expectedCount, true)
));
}
$realCount = $profile->getCollector('swiftmailer')->getMessageCount();
if ($expectedCount === null) {
$this->assertGreaterThan(0, $realCount);
} else {
$this->assertEquals(
$expectedCount,
$realCount,
sprintf(
'Expected number of sent emails was %d, but in reality %d %s sent.',
$expectedCount,
$realCount,
$realCount === 2 ? 'was' : 'were'
)
);
}
}
/**
* Checks that no email was sent. This is an alias for seeEmailIsSent(0).
*
* @part email
*/
public function dontSeeEmailIsSent()
{
$this->seeEmailIsSent(0);
}
/**
* Grabs a service from Symfony DIC container.
* Recommended to use for unit testing.
*
* ``` php
* <?php
* $em = $I->grabServiceFromContainer('doctrine');
* ?>
* ```
*
* @param $service
* @return mixed
* @part services
* @deprecated Use grabService instead
*/
public function grabServiceFromContainer($service)
{
return $this->grabService($service);
}
/**
* Grabs a service from Symfony DIC container.
* Recommended to use for unit testing.
*
* ``` php
* <?php
* $em = $I->grabService('doctrine');
* ?>
* ```
*
* @param $service
* @return mixed
* @part services
*/
public function grabService($service)
{
$container = $this->_getContainer();
if (!$container->has($service)) {
$this->fail("Service $service is not available in container");
}
return $container->get($service);
}
/**
* @return \Symfony\Component\HttpKernel\Profiler\Profile
*/
protected function getProfile()
{
$container = $this->_getContainer();
if (!$container->has('profiler')) {
return null;
}
$profiler = $this->grabService('profiler');
$response = $this->client->getResponse();
if (null === $response) {
$this->fail("You must perform a request before using this method.");
}
return $profiler->loadProfileFromResponse($response);
}
/**
* @param $url
*/
protected function debugResponse($url)
{
parent::debugResponse($url);
if ($profile = $this->getProfile()) {
if ($profile->hasCollector('security')) {
if ($profile->getCollector('security')->isAuthenticated()) {
$roles = $profile->getCollector('security')->getRoles();
if ($roles instanceof Data) {
$roles = $this->extractRawRoles($roles);
}
$this->debugSection(
'User',
$profile->getCollector('security')->getUser()
. ' [' . implode(',', $roles) . ']'
);
} else {
$this->debugSection('User', 'Anonymous');
}
}
if ($profile->hasCollector('swiftmailer')) {
$messages = $profile->getCollector('swiftmailer')->getMessageCount();
if ($messages) {
$this->debugSection('Emails', $messages . ' sent');
}
}
if ($profile->hasCollector('timer')) {
$this->debugSection('Time', $profile->getCollector('timer')->getTime());
}
}
}
/**
* @param Data $data
* @return array
*/
private function extractRawRoles(Data $data)
{
if ($this->dataRevealsValue($data)) {
$roles = $data->getValue();
} else {
$raw = $data->getRawData();
$roles = isset($raw[1]) ? $raw[1] : [];
}
return $roles;
}
/**
* Returns a list of recognized domain names.
*
* @return array
*/
protected function getInternalDomains()
{
$internalDomains = [];
$routes = $this->grabService('router')->getRouteCollection();
/* @var \Symfony\Component\Routing\Route $route */
foreach ($routes as $route) {
if (!is_null($route->getHost())) {
$compiled = $route->compile();
if (!is_null($compiled->getHostRegex())) {
$internalDomains[] = $compiled->getHostRegex();
}
}
}
return array_unique($internalDomains);
}
/**
* Reboot client's kernel.
* Can be used to manually reboot kernel when 'rebootable_client' => false
*
* ``` php
* <?php
* ...
* perform some requests
* ...
* $I->rebootClientKernel();
* ...
* perform other requests
* ...
*
* ?>
* ```
*
*/
public function rebootClientKernel()
{
if ($this->client) {
$this->client->rebootKernel();
}
}
/**
* Public API from Data changed from Symfony 3.2 to 3.3.
*
* @param \Symfony\Component\VarDumper\Cloner\Data $data
*
* @return bool
*/
private function dataRevealsValue(Data $data)
{
return method_exists($data, 'getValue');
}
/**
* Returns list of the possible kernel classes based on the module configuration
*
* @return array
*/
private function getPossibleKernelClasses()
{
if (empty($this->config['kernel_class'])) {
return self::$possibleKernelClasses;
}
if (!is_string($this->config['kernel_class'])) {
throw new ModuleException(
__CLASS__,
"Parameter 'kernel_class' must have 'string' type.\n"
);
}
return [$this->config['kernel_class']];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Interfaces\API;
use Codeception\Module as CodeceptionModule;
use Codeception\Lib\Framework;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleRequireException;
use Codeception\TestInterface;
/**
* Module for testing XMLRPC WebService.
*
* This module can be used either with frameworks or PHPBrowser.
* It tries to guess the framework is is attached to.
*
* Whether framework is used it operates via standard framework modules.
* Otherwise sends raw HTTP requests to url via PHPBrowser.
*
* ## Requirements
*
* * Module requires installed php_xmlrpc extension
*
* ## Status
*
* * Maintainer: **tiger-seo**
* * Stability: **beta**
* * Contact: tiger.seo@gmail.com
*
* ## Configuration
*
* * url *optional* - the url of api
*
* ## Public Properties
*
* * headers - array of headers going to be sent.
* * params - array of sent data
* * response - last response (string)
*
* @since 1.1.5
* @author tiger.seo@gmail.com
*/
class XMLRPC extends CodeceptionModule implements API
{
protected $config = ['url' => ""];
/**
* @var \Symfony\Component\BrowserKit\Client
*/
public $client = null;
public $is_functional = false;
public $headers = [];
public $params = [];
public $response = "";
public function _initialize()
{
if (!function_exists('xmlrpc_encode_request')) {
throw new ModuleRequireException(__CLASS__, "XMLRPC module requires installed php_xmlrpc extension");
}
parent::_initialize();
}
public function _before(TestInterface $test)
{
if (!$this->client) {
if (!strpos($this->config['url'], '://')) {
// not valid url
foreach ($this->getModules() as $module) {
if ($module instanceof Framework) {
$this->client = $module->client;
$this->is_functional = true;
break;
}
}
} else {
if (!$this->hasModule('PhpBrowser')) {
throw new ModuleConfigException(
__CLASS__,
"For XMLRPC testing via HTTP please enable PhpBrowser module"
);
}
$this->client = $this->getModule('PhpBrowser')->client;
}
if (!$this->client) {
throw new ModuleConfigException(
__CLASS__,
"Client for XMLRPC requests not initialized.\n"
. "Provide either PhpBrowser module, or a framework module which shares FrameworkInterface"
);
}
}
$this->headers = [];
$this->params = [];
$this->response = '';
$this->client->setServerParameters([]);
}
/**
* Sets HTTP header
*
* @param string $name
* @param string $value
*/
public function haveHttpHeader($name, $value)
{
$this->headers[$name] = $value;
}
/**
* Checks response code.
*
* @param $num
*/
public function seeResponseCodeIs($num)
{
\PHPUnit\Framework\Assert::assertEquals($num, $this->client->getInternalResponse()->getStatus());
}
/**
* Checks weather last response was valid XMLRPC.
* This is done with xmlrpc_decode function.
*
*/
public function seeResponseIsXMLRPC()
{
$result = xmlrpc_decode($this->response);
\PHPUnit\Framework\Assert::assertNotNull($result, 'Invalid response document returned from XmlRpc server');
}
/**
* Sends a XMLRPC method call to remote XMLRPC-server.
*
* @param string $methodName
* @param array $parameters
*/
public function sendXMLRPCMethodCall($methodName, $parameters = [])
{
if (!array_key_exists('Content-Type', $this->headers)) {
$this->headers['Content-Type'] = 'text/xml';
}
foreach ($this->headers as $header => $val) {
$this->client->setServerParameter("HTTP_$header", $val);
}
$url = $this->config['url'];
if (is_array($parameters)) {
$parameters = $this->scalarizeArray($parameters);
}
$requestBody = xmlrpc_encode_request($methodName, array_values($parameters));
$this->debugSection('Request', $url . PHP_EOL . $requestBody);
$this->client->request('POST', $url, [], [], [], $requestBody);
$this->response = $this->client->getInternalResponse()->getContent();
$this->debugSection('Response', $this->response);
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Framework;
use Codeception\Exception\ModuleConfigException;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\TestInterface;
use Codeception\Lib\Connector\Yii1 as Yii1Connector;
use Codeception\Util\ReflectionHelper;
use Yii;
/**
* This module provides integration with [Yii Framework 1.1](http://www.yiiframework.com/doc/guide/).
*
* The following configurations are available for this module:
*
* * `appPath` - full path to the application, include index.php</li>
* * `url` - full url to the index.php entry script</li>
*
* In your index.php you must return an array with correct configuration for the application:
*
* For the simple created yii application index.php will be like this:
*
* ```php
* <?php
* // change the following paths if necessary
* $yii=dirname(__FILE__).'/../yii/framework/yii.php';
* $config=dirname(__FILE__).'/protected/config/main.php';
*
* // remove the following lines when in production mode
* defined('YII_DEBUG') or define('YII_DEBUG',true);
* // specify how many levels of call stack should be shown in each log message
* defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3);
* require_once($yii);
* return array(
* 'class' => 'CWebApplication',
* 'config' => $config,
* );
* ```
*
* You can use this module by setting params in your `functional.suite.yml`:
*
* ```yaml
* actor: FunctionalTester
* modules:
* enabled:
* - Yii1:
* appPath: '/path/to/index.php'
* url: 'http://localhost/path/to/index.php'
* - \Helper\Functional
* ```
*
* You will also need to install [Codeception-Yii Bridge](https://github.com/Codeception/YiiBridge)
* which include component wrappers for testing.
*
* When you are done, you can test this module by creating new empty Yii application and creating this Cept scenario:
*
* ```
* php codecept.phar g:cept functional IndexCept
* ```
*
* and write it as in example:
*
* ```php
* <?php
* $I = new FunctionalTester($scenario);
* $I->wantTo('Test index page');
* $I->amOnPage('/index.php');
* $I->see('My Web Application','#header #logo');
* $I->click('Login');
* $I->see('Login','h1');
* $I->see('Username');
* $I->fillField('#LoginForm_username','demo');
* $I->fillField('#LoginForm_password','demo');
* $I->click('#login-form input[type="submit"]');
* $I->seeLink('Logout (demo)');
* $I->click('Logout (demo)');
* $I->seeLink('Login');
* ```
*
* Then run codeception: php codecept.phar --steps run functional
* You must see "OK" and that all steps are marked with asterisk (*).
* Do not forget that after adding module in your functional.suite.yml you must run codeception "build" command.
*
* ### Public Properties
*
* `client`: instance of `\Codeception\Lib\Connector\Yii1`
*
* ### Parts
*
* If you ever encounter error message:
*
* ```
* Yii1 module conflicts with WebDriver
* ```
*
* you should include Yii module partially, with `init` part only
*
* * `init`: only initializes module and not provides any actions from it. Can be used for unit/acceptance tests to avoid conflicts.
*
* ### Acceptance Testing Example:
*
* In `acceptance.suite.yml`:
*
* ```yaml
* class_name: AcceptanceTester
* modules:
* enabled:
* - WebDriver:
* browser: firefox
* url: http://localhost
* - Yii1:
* appPath: '/path/to/index.php'
* url: 'http://localhost/path/to/index.php'
* part: init # to not conflict with WebDriver
* - \Helper\Acceptance
* ```
*/
class Yii1 extends Framework implements PartedModule
{
/**
* Application path and url must be set always
* @var array
*/
protected $requiredFields = ['appPath', 'url'];
/**
* Application settings array('class'=>'YourAppClass','config'=>'YourAppArrayConfig');
* @var array
*/
private $appSettings;
private $_appConfig;
public function _initialize()
{
if (!file_exists($this->config['appPath'])) {
throw new ModuleConfigException(
__CLASS__,
"Couldn't load application config file {$this->config['appPath']}\n" .
"Please provide application bootstrap file configured for testing"
);
}
$this->appSettings = include($this->config['appPath']); //get application settings in the entry script
// get configuration from array or file
if (is_array($this->appSettings['config'])) {
$this->_appConfig = $this->appSettings['config'];
} else {
if (!file_exists($this->appSettings['config'])) {
throw new ModuleConfigException(
__CLASS__,
"Couldn't load configuration file from Yii app file: {$this->appSettings['config']}\n" .
"Please provide valid 'config' parameter"
);
}
$this->_appConfig = include($this->appSettings['config']);
}
if (!defined('YII_ENABLE_EXCEPTION_HANDLER')) {
define('YII_ENABLE_EXCEPTION_HANDLER', false);
}
if (!defined('YII_ENABLE_ERROR_HANDLER')) {
define('YII_ENABLE_ERROR_HANDLER', false);
}
$_SERVER['SCRIPT_NAME'] = parse_url($this->config['url'], PHP_URL_PATH);
$_SERVER['SCRIPT_FILENAME'] = $this->config['appPath'];
if (!function_exists('launch_codeception_yii_bridge')) {
throw new ModuleConfigException(
__CLASS__,
"Codeception-Yii Bridge is not launched. In order to run tests you need to install "
. "https://github.com/Codeception/YiiBridge Implement function 'launch_codeception_yii_bridge' to "
. "load all Codeception overrides"
);
}
launch_codeception_yii_bridge();
Yii::$enableIncludePath = false;
Yii::setApplication(null);
Yii::createApplication($this->appSettings['class'], $this->_appConfig);
}
/*
* Create the client connector. Called before each test
*/
public function _createClient()
{
$this->client = new Yii1Connector();
$this->client->setServerParameter("HTTP_HOST", parse_url($this->config['url'], PHP_URL_HOST));
$this->client->appPath = $this->config['appPath'];
$this->client->url = $this->config['url'];
$this->client->appSettings = [
'class' => $this->appSettings['class'],
'config' => $this->_appConfig,
];
}
public function _before(TestInterface $test)
{
$this->_createClient();
}
public function _after(TestInterface $test)
{
$_SESSION = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$_REQUEST = [];
Yii::app()->session->close();
parent::_after($test);
}
/**
* Getting domain regex from rule template and parameters
*
* @param string $template
* @param array $parameters
* @return string
*/
private function getDomainRegex($template, $parameters = [])
{
if ($host = parse_url($template, PHP_URL_HOST)) {
$template = $host;
}
if (strpos($template, '<') !== false) {
$template = str_replace(['<', '>'], '#', $template);
}
$template = preg_quote($template);
foreach ($parameters as $name => $value) {
$template = str_replace("#$name#", $value, $template);
}
return '/^' . $template . '$/u';
}
/**
* Returns a list of regex patterns for recognized domain names
*
* @return array
*/
public function getInternalDomains()
{
$domains = [$this->getDomainRegex(Yii::app()->request->getHostInfo())];
if (Yii::app()->urlManager->urlFormat === 'path') {
$parent = Yii::app()->urlManager instanceof \CUrlManager ? '\CUrlManager' : null;
$rules = ReflectionHelper::readPrivateProperty(Yii::app()->urlManager, '_rules', $parent);
foreach ($rules as $rule) {
if ($rule->hasHostInfo === true) {
$domains[] = $this->getDomainRegex($rule->template, $rule->params);
}
}
}
return array_unique($domains);
}
public function _parts()
{
return ['init', 'initialize'];
}
}

View File

@@ -0,0 +1,855 @@
<?php
namespace Codeception\Module;
use Codeception\Configuration;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Connector\Yii2 as Yii2Connector;
use Codeception\Lib\Framework;
use Codeception\Lib\Interfaces\ActiveRecord;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\TestInterface;
use Codeception\Util\Debug;
use Yii;
use yii\base\Event;
use yii\db\ActiveRecordInterface;
use yii\db\Connection;
use yii\db\QueryInterface;
use yii\db\Transaction;
/**
* This module provides integration with [Yii framework](http://www.yiiframework.com/) (2.0).
* It initializes Yii framework in test environment and provides actions for functional testing.
* ## Application state during testing
* This section details what you can expect when using this module.
* * You will get a fresh application in `\Yii::$app` at the start of each test (available in the test and in `_before()`).
* * Inside your test you may change application state; however these changes will be lost when doing a request if you have enabled `recreateApplication`.
* * When executing a request via one of the request functions the `request` and `response` component are both recreated.
* * After a request the whole application is available for inspection / interaction.
* * You may use multiple database connections, each will use a separate transaction; to prevent accidental mistakes we
* will warn you if you try to connect to the same database twice but we cannot reuse the same connection.
*
* ## Config
*
* * `configFile` *required* - the path to the application config file. File should be configured for test environment and return configuration array.
* * `entryUrl` - initial application url (default: http://localhost/index-test.php).
* * `entryScript` - front script title (like: index-test.php). If not set - taken from entryUrl.
* * `transaction` - (default: true) wrap all database connection inside a transaction and roll it back after the test. Should be disabled for acceptance testing..
* * `cleanup` - (default: true) cleanup fixtures after the test
* * `ignoreCollidingDSN` - (default: false) When 2 database connections use the same DSN but different settings an exception will be thrown, set this to true to disable this behavior.
* * `fixturesMethod` - (default: _fixtures) Name of the method used for creating fixtures.
* * `responseCleanMethod` - (default: clear) Method for cleaning the response object. Note that this is only for multiple requests inside a single test case.
* Between test casesthe whole application is always recreated
* * `requestCleanMethod` - (default: recreate) Method for cleaning the request object. Note that this is only for multiple requests inside a single test case.
* Between test cases the whole application is always recreated
* * `recreateComponents` - (default: []) Some components change their state making them unsuitable for processing multiple requests. In production this is usually
* not a problem since web apps tend to die and start over after each request. This allows you to list application components that need to be recreated before each request.
* As a consequence, any components specified here should not be changed inside a test since those changes will get regarded.
* You can use this module by setting params in your functional.suite.yml:
* * `recreateApplication` - (default: false) whether to recreate the whole application before each request
* You can use this module by setting params in your functional.suite.yml:
* ```yaml
* actor: FunctionalTester
* modules:
* enabled:
* - Yii2:
* configFile: 'path/to/config.php'
* ```
*
* ### Parts
*
* By default all available methods are loaded, but you can specify parts to select only needed actions and avoid conflicts.
*
* * `init` - use module only for initialization (for acceptance tests).
* * `orm` - include only `haveRecord/grabRecord/seeRecord/dontSeeRecord` actions.
* * `fixtures` - use fixtures inside tests with `haveFixtures/grabFixture/grabFixtures` actions.
* * `email` - include email actions `seeEmailsIsSent/grabLastSentEmail/...`
*
* ### Example (`functional.suite.yml`)
*
* ```yaml
* actor: FunctionalTester
* modules:
* enabled:
* - Yii2:
* configFile: 'config/test.php'
* ```
*
* ### Example (`unit.suite.yml`)
*
* ```yaml
* actor: UnitTester
* modules:
* enabled:
* - Asserts
* - Yii2:
* configFile: 'config/test.php'
* part: init
* ```
*
* ### Example (`acceptance.suite.yml`)
*
* ```yaml
* actor: AcceptanceTester
* modules:
* enabled:
* - WebDriver:
* url: http://127.0.0.1:8080/
* browser: firefox
* - Yii2:
* configFile: 'config/test.php'
* part: ORM # allow to use AR methods
* transaction: false # don't wrap test in transaction
* cleanup: false # don't cleanup the fixtures
* entryScript: index-test.php
* ```
*
* ## Fixtures
*
* This module allows to use [fixtures](http://www.yiiframework.com/doc-2.0/guide-test-fixtures.html) inside a test. There are two options for that.
* Fixtures can be loaded using [haveFixtures](#haveFixtures) method inside a test:
*
* ```php
* <?php
* $I->haveFixtures(['posts' => PostsFixture::className()]);
* ```
*
* or, if you need to load fixtures before the test, you
* can specify fixtures with `_fixtures` method of a testcase:
*
* ```php
* <?php
* // inside Cest file or Codeception\TestCase\Unit
* public function _fixtures()
* {
* return ['posts' => PostsFixture::className()]
* }
* ```
*
* ## URL
* This module provide to use native URL formats of Yii2 for all codeception commands that use url for work.
* This commands allows input like:
*
* ```php
* <?php
* $I->amOnPage(['site/view','page'=>'about']);
* $I->amOnPage('index-test.php?site/index');
* $I->amOnPage('http://localhost/index-test.php?site/index');
* $I->sendAjaxPostRequest(['/user/update', 'id' => 1], ['UserForm[name]' => 'G.Hopper');
* ```
*
* ## Status
*
* Maintainer: **samdark**
* Stability: **stable**
*
* @property \Codeception\Lib\Connector\Yii2 $client
*/
class Yii2 extends Framework implements ActiveRecord, PartedModule
{
/**
* Application config file must be set.
* @var array
*/
protected $config = [
'fixturesMethod' => '_fixtures',
'cleanup' => true,
'ignoreCollidingDSN' => false,
'transaction' => null,
'entryScript' => '',
'entryUrl' => 'http://localhost/index-test.php',
'responseCleanMethod' => Yii2Connector::CLEAN_CLEAR,
'requestCleanMethod' => Yii2Connector::CLEAN_RECREATE,
'recreateComponents' => [],
'recreateApplication' => false
];
protected $requiredFields = ['configFile'];
/**
* @var Yii2Connector\FixturesStore[]
*/
public $loadedFixtures = [];
/**
* Helper to manage database connections
* @var Yii2Connector\ConnectionWatcher
*/
private $connectionWatcher;
/**
* Helper to force database transaction
* @var Yii2Connector\TransactionForcer
*/
private $transactionForcer;
/**
* @var array The contents of $_SERVER upon initialization of this object.
* This is only used to restore it upon object destruction.
* It MUST not be used anywhere else.
*/
private $server;
public function _initialize()
{
if ($this->config['transaction'] === null) {
$this->config['transaction'] = $this->backupConfig['transaction'] = $this->config['cleanup'];
}
$this->defineConstants();
$this->server = $_SERVER;
$this->initServerGlobal();
}
/**
* Module configuration changed inside a test.
* We always re-create the application.
*/
protected function onReconfigure()
{
parent::onReconfigure();
$this->client->resetApplication();
$this->configureClient($this->config);
$this->client->startApp();
}
/**
* Adds the required server params.
* Note this is done separately from the request cycle since someone might call
* `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
*/
private function initServerGlobal()
{
$entryUrl = $this->config['entryUrl'];
$entryFile = $this->config['entryScript'] ?: basename($entryUrl);
$entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH);
$_SERVER = array_merge($_SERVER, [
'SCRIPT_FILENAME' => $entryFile,
'SCRIPT_NAME' => $entryScript,
'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST),
'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80',
'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https'
]);
}
protected function validateConfig()
{
parent::validateConfig();
$pathToConfig = codecept_absolute_path($this->config['configFile']);
if (!is_file($pathToConfig)) {
throw new ModuleConfigException(
__CLASS__,
"The application config file does not exist: " . $pathToConfig
);
}
if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS)) {
throw new ModuleConfigException(
__CLASS__,
"The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS)
);
}
if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS)) {
throw new ModuleConfigException(
__CLASS__,
"The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS)
);
}
}
protected function configureClient(array $settings)
{
$settings['configFile'] = codecept_absolute_path($settings['configFile']);
foreach ($settings as $key => $value) {
if (property_exists($this->client, $key)) {
$this->client->$key = $value;
}
}
$this->client->resetApplication();
}
/**
* Instantiates the client based on module configuration
*/
protected function recreateClient()
{
$entryUrl = $this->config['entryUrl'];
$entryFile = $this->config['entryScript'] ?: basename($entryUrl);
$entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH);
$this->client = new Yii2Connector([
'SCRIPT_FILENAME' => $entryFile,
'SCRIPT_NAME' => $entryScript,
'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST),
'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80',
'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https'
]);
$this->configureClient($this->config);
}
public function _before(TestInterface $test)
{
$this->recreateClient();
$this->client->startApp();
$this->connectionWatcher = new Yii2Connector\ConnectionWatcher();
$this->connectionWatcher->start();
// load fixtures before db transaction
if ($test instanceof \Codeception\Test\Cest) {
$this->loadFixtures($test->getTestClass());
} else {
$this->loadFixtures($test);
}
$this->startTransactions();
}
/**
* load fixtures before db transaction
*
* @param mixed $test instance of test class
*/
private function loadFixtures($test)
{
$this->debugSection('Fixtures', 'Loading fixtures');
if (empty($this->loadedFixtures)
&& method_exists($test, $this->_getConfig('fixturesMethod'))
) {
$connectionWatcher = new Yii2Connector\ConnectionWatcher();
$connectionWatcher->start();
$this->haveFixtures(call_user_func([$test, $this->_getConfig('fixturesMethod')]));
$connectionWatcher->stop();
$connectionWatcher->closeAll();
}
$this->debugSection('Fixtures', 'Done');
}
public function _after(TestInterface $test)
{
$_SESSION = [];
$_FILES = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$_REQUEST = [];
$this->rollbackTransactions();
$this->connectionWatcher->stop();
$this->connectionWatcher->closeAll();
unset($this->connectionWatcher);
if ($this->config['cleanup']) {
foreach ($this->loadedFixtures as $fixture) {
$fixture->unloadFixtures();
}
$this->loadedFixtures = [];
}
if ($this->client !== null && $this->client->getApplication()->has('session', true)) {
$this->client->getApplication()->session->close();
}
$this->client->resetApplication();
parent::_after($test);
}
protected function startTransactions()
{
if ($this->config['transaction']) {
$this->transactionForcer = new Yii2Connector\TransactionForcer($this->config['ignoreCollidingDSN']);
$this->transactionForcer->start();
}
}
protected function rollbackTransactions()
{
if (isset($this->transactionForcer)) {
$this->transactionForcer->rollbackAll();
$this->transactionForcer->stop();
unset($this->transactionForcer);
}
}
public function _parts()
{
return ['orm', 'init', 'fixtures', 'email'];
}
/**
* Authorizes user on a site without submitting login form.
* Use it for fast pragmatic authorization in functional tests.
*
* ```php
* <?php
* // User is found by id
* $I->amLoggedInAs(1);
*
* // User object is passed as parameter
* $admin = \app\models\User::findByUsername('admin');
* $I->amLoggedInAs($admin);
* ```
* Requires `user` component to be enabled and configured.
*
* @param $user
* @throws ModuleException
*/
public function amLoggedInAs($user)
{
if (!$this->client->getApplication()->has('user')) {
throw new ModuleException($this, 'User component is not loaded');
}
if ($user instanceof \yii\web\IdentityInterface) {
$identity = $user;
} else {
// class name implementing IdentityInterface
$identityClass = $this->client->getApplication()->user->identityClass;
$identity = call_user_func([$identityClass, 'findIdentity'], $user);
}
$this->client->getApplication()->user->login($identity);
}
/**
* Creates and loads fixtures from a config.
* Signature is the same as for `fixtures()` method of `yii\test\FixtureTrait`
*
* ```php
* <?php
* $I->haveFixtures([
* 'posts' => PostsFixture::className(),
* 'user' => [
* 'class' => UserFixture::className(),
* 'dataFile' => '@tests/_data/models/user.php',
* ],
* ]);
* ```
*
* Note: if you need to load fixtures before the test (probably before the cleanup transaction is started;
* `cleanup` options is `true` by default), you can specify fixtures with _fixtures method of a testcase
* ```php
* <?php
* // inside Cest file or Codeception\TestCase\Unit
* public function _fixtures(){
* return [
* 'user' => [
* 'class' => UserFixture::className(),
* 'dataFile' => codecept_data_dir() . 'user.php'
* ]
* ];
* }
* ```
* instead of defining `haveFixtures` in Cest `_before`
*
* @param $fixtures
* @part fixtures
*/
public function haveFixtures($fixtures)
{
if (empty($fixtures)) {
return;
}
$fixturesStore = new Yii2Connector\FixturesStore($fixtures);
$fixturesStore->unloadFixtures();
$fixturesStore->loadFixtures();
$this->loadedFixtures[] = $fixturesStore;
}
/**
* Returns all loaded fixtures.
* Array of fixture instances
*
* @part fixtures
* @return array
*/
public function grabFixtures()
{
return call_user_func_array(
'array_merge',
array_map( // merge all fixtures from all fixture stores
function ($fixturesStore) {
return $fixturesStore->getFixtures();
},
$this->loadedFixtures
)
);
}
/**
* Gets a fixture by name.
* Returns a Fixture instance. If a fixture is an instance of `\yii\test\BaseActiveFixture` a second parameter
* can be used to return a specific model:
*
* ```php
* <?php
* $I->haveFixtures(['users' => UserFixture::className()]);
*
* $users = $I->grabFixture('users');
*
* // get first user by key, if a fixture is instance of ActiveFixture
* $user = $I->grabFixture('users', 'user1');
* ```
*
* @param $name
* @return mixed
* @throws ModuleException if a fixture is not found
* @part fixtures
*/
public function grabFixture($name, $index = null)
{
$fixtures = $this->grabFixtures();
if (!isset($fixtures[$name])) {
throw new ModuleException($this, "Fixture $name is not loaded");
}
$fixture = $fixtures[$name];
if ($index === null) {
return $fixture;
}
if ($fixture instanceof \yii\test\BaseActiveFixture) {
return $fixture->getModel($index);
}
throw new ModuleException($this, "Fixture $name is not an instance of ActiveFixture and can't be loaded with second parameter");
}
/**
* Inserts record into the database.
*
* ``` php
* <?php
* $user_id = $I->haveRecord('app\models\User', array('name' => 'Davert'));
* ?>
* ```
*
* @param $model
* @param array $attributes
* @return mixed
* @part orm
*/
public function haveRecord($model, $attributes = [])
{
/** @var $record \yii\db\ActiveRecord * */
$record = \Yii::createObject($model);
$record->setAttributes($attributes, false);
$res = $record->save(false);
if (!$res) {
$this->fail("Record $model was not saved");
}
return $record->primaryKey;
}
/**
* Checks that record exists in database.
*
* ``` php
* $I->seeRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param $model
* @param array $attributes
* @part orm
*/
public function seeRecord($model, $attributes = [])
{
$record = $this->findRecord($model, $attributes);
if (!$record) {
$this->fail("Couldn't find $model with " . json_encode($attributes));
}
$this->debugSection($model, json_encode($record));
}
/**
* Checks that record does not exist in database.
*
* ``` php
* $I->dontSeeRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param $model
* @param array $attributes
* @part orm
*/
public function dontSeeRecord($model, $attributes = [])
{
$record = $this->findRecord($model, $attributes);
$this->debugSection($model, json_encode($record));
if ($record) {
$this->fail("Unexpectedly managed to find $model with " . json_encode($attributes));
}
}
/**
* Retrieves record from database
*
* ``` php
* $category = $I->grabRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param $model
* @param array $attributes
* @return mixed
* @part orm
*/
public function grabRecord($model, $attributes = [])
{
return $this->findRecord($model, $attributes);
}
/**
* @param string $model Class name
* @param array $attributes
* @return mixed
*/
protected function findRecord($model, $attributes = [])
{
if (!class_exists($model)) {
throw new \RuntimeException("Class $model does not exist");
}
$rc = new \ReflectionClass($model);
if ($rc->hasMethod('find')
&& ($findMethod = $rc->getMethod('find'))
&& $findMethod->isStatic()
&& $findMethod->isPublic()
&& $findMethod->getNumberOfRequiredParameters() === 0
) {
$activeQuery = $findMethod->invoke(null);
if ($activeQuery instanceof QueryInterface) {
return $activeQuery->andWhere($attributes)->one();
}
throw new \RuntimeException("$model::find() must return an instance of yii\db\QueryInterface");
}
throw new \RuntimeException("Class $model does not have a public static find() method without required parameters");
}
/**
* Similar to amOnPage but accepts route as first argument and params as second
*
* ```
* $I->amOnRoute('site/view', ['page' => 'about']);
* ```
*
*/
public function amOnRoute($route, array $params = [])
{
array_unshift($params, $route);
$this->amOnPage($params);
}
/**
* To support to use the behavior of urlManager component
* for the methods like this: amOnPage(), sendAjaxRequest() and etc.
* @param $method
* @param $uri
* @param array $parameters
* @param array $files
* @param array $server
* @param null $content
* @param bool $changeHistory
* @return mixed
*/
protected function clientRequest($method, $uri, array $parameters = [], array $files = [], array $server = [], $content = null, $changeHistory = true)
{
if (is_array($uri)) {
$uri = $this->client->getApplication()->getUrlManager()->createUrl($uri);
}
return parent::clientRequest($method, $uri, $parameters, $files, $server, $content, $changeHistory);
}
/**
* Gets a component from Yii container. Throws exception if component is not available
*
* ```php
* <?php
* $mailer = $I->grabComponent('mailer');
* ```
*
* @param $component
* @return mixed
* @throws ModuleException
*/
public function grabComponent($component)
{
if (!$this->client->getApplication()->has($component)) {
throw new ModuleException($this, "Component $component is not available in current application");
}
return $this->client->getApplication()->get($component);
}
/**
* Checks that email is sent.
*
* ```php
* <?php
* // check that at least 1 email was sent
* $I->seeEmailIsSent();
*
* // check that only 3 emails were sent
* $I->seeEmailIsSent(3);
* ```
*
* @param int $num
* @throws ModuleException
* @part email
*/
public function seeEmailIsSent($num = null)
{
if ($num === null) {
$this->assertNotEmpty($this->grabSentEmails(), 'emails were sent');
return;
}
$this->assertEquals($num, count($this->grabSentEmails()), 'number of sent emails is equal to ' . $num);
}
/**
* Checks that no email was sent
*
* @part email
*/
public function dontSeeEmailIsSent()
{
$this->seeEmailIsSent(0);
}
/**
* Returns array of all sent email messages.
* Each message implements `yii\mail\MessageInterface` interface.
* Useful to perform additional checks using `Asserts` module:
*
* ```php
* <?php
* $I->seeEmailIsSent();
* $messages = $I->grabSentEmails();
* $I->assertEquals('admin@site,com', $messages[0]->getTo());
* ```
*
* @part email
* @return array
* @throws ModuleException
*/
public function grabSentEmails()
{
$mailer = $this->grabComponent('mailer');
if (!$mailer instanceof Yii2Connector\TestMailer) {
throw new ModuleException($this, "Mailer module is not mocked, can't test emails");
}
return $mailer->getSentMessages();
}
/**
* Returns last sent email:
*
* ```php
* <?php
* $I->seeEmailIsSent();
* $message = $I->grabLastSentEmail();
* $I->assertEquals('admin@site,com', $message->getTo());
* ```
* @part email
*/
public function grabLastSentEmail()
{
$this->seeEmailIsSent();
$messages = $this->grabSentEmails();
return end($messages);
}
/**
* Getting domain regex from rule host template
*
* @param string $template
* @return string
*/
private function getDomainRegex($template)
{
if (preg_match('#https?://(.*)#', $template, $matches)) {
$template = $matches[1];
}
$parameters = [];
if (strpos($template, '<') !== false) {
$template = preg_replace_callback(
'/<(?:\w+):?([^>]+)?>/u',
function ($matches) use (&$parameters) {
$key = '#' . count($parameters) . '#';
$parameters[$key] = isset($matches[1]) ? $matches[1] : '\w+';
return $key;
},
$template
);
}
$template = preg_quote($template);
$template = strtr($template, $parameters);
return '/^' . $template . '$/u';
}
/**
* Returns a list of regex patterns for recognized domain names
*
* @return array
*/
public function getInternalDomains()
{
$domains = [$this->getDomainRegex($this->client->getApplication()->urlManager->hostInfo)];
if ($this->client->getApplication()->urlManager->enablePrettyUrl) {
foreach ($this->client->getApplication()->urlManager->rules as $rule) {
/** @var \yii\web\UrlRule $rule */
if (isset($rule->host)) {
$domains[] = $this->getDomainRegex($rule->host);
}
}
}
return array_unique($domains);
}
private function defineConstants()
{
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', false);
}
/**
* Sets a cookie and, if validation is enabled, signs it.
* @param string $name The name of the cookie
* @param string $value The value of the cookie
* @param array $params Additional cookie params like `domain`, `path`, `expires` and `secure`.
*/
public function setCookie($name, $val, array $params = [])
{
// Sign the cookie.
if ($this->client->getApplication()->request->enableCookieValidation) {
$val = $this->client->getApplication()->security->hashData(serialize([$name, $val]), $this->client->getApplication()->request->cookieValidationKey);
}
parent::setCookie($name, $val, $params);
}
/**
* This function creates the CSRF Cookie.
* @param string $val The value of the CSRF token
* @return string[] Returns an array containing the name of the CSRF param and the masked CSRF token.
*/
public function createAndSetCsrfCookie($val)
{
$masked = $this->client->getApplication()->security->maskToken($val);
$name = $this->client->getApplication()->request->csrfParam;
$this->setCookie($name, $val);
return [$name, $masked];
}
public function _afterSuite()
{
parent::_afterSuite();
codecept_debug('Suite done, restoring $_SERVER to original');
$_SERVER = $this->server;
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace Codeception\Module;
use Codeception\Configuration;
use Codeception\Lib\Framework;
use Codeception\TestInterface;
use Codeception\Exception\ModuleException;
use Codeception\Util\ReflectionHelper;
use Codeception\Lib\Connector\ZF1 as ZF1Connector;
use Zend_Controller_Router_Route_Hostname;
use Zend_Controller_Router_Route_Chain;
/**
* This module allows you to run tests inside Zend Framework.
* It acts just like ControllerTestCase, but with usage of Codeception syntax.
*
* It assumes, you have standard structure with __APPLICATION_PATH__ set to './application'
* and LIBRARY_PATH set to './library'. If it's not then set the appropriate path in the Config.
*
* [Tutorial](http://codeception.com/01-27-2012/bdd-with-zend-framework.html)
*
* ## Status
*
* * Maintainer: **davert**
* * Stability: **stable**
* * Contact: codecept@davert.mail.ua
*
* ## Config
*
* * env - environment used for testing ('testing' by default).
* * config - relative path to your application config ('application/configs/application.ini' by default).
* * app_path - relative path to your application folder ('application' by default).
* * lib_path - relative path to your library folder ('library' by default).
*
* ## API
*
* * client - BrowserKit client
* * db - current instance of Zend_Db_Adapter
* * bootstrap - current bootstrap file.
*
* ## Cleaning up
*
* Unfortunately Zend_Db doesn't support nested transactions,
* thus, for cleaning your database you should either use standard Db module or
* [implement nested transactions yourself](http://blog.ekini.net/2010/03/05/zend-framework-how-to-use-nested-transactions-with-zend_db-and-mysql/).
*
* If your database supports nested transactions (MySQL doesn't)
* or you implemented them you can put all your code inside a transaction.
* Use a generated helper TestHelper. Use this code inside of it.
*
* ``` php
* <?php
* namespace Codeception\Module;
* class TestHelper extends \Codeception\Module {
* function _before($test) {
* $this->getModule('ZF1')->db->beginTransaction();
* }
*
* function _after($test) {
* $this->getModule('ZF1')->db->rollback();
* }
* }
* ?>
* ```
*
* This will make your functional tests run super-fast.
*
*/
class ZF1 extends Framework
{
protected $config = [
'env' => 'testing',
'config' => 'application/configs/application.ini',
'app_path' => 'application',
'lib_path' => 'library'
];
/**
* @var \Zend_Application
*/
public $bootstrap;
/**
* @var \Zend_Db_Adapter_Abstract
*/
public $db;
/**
* @var \Codeception\Lib\Connector\ZF1
*/
public $client;
protected $queries = 0;
protected $time = 0;
/**
* @var array Used to collect domains while recursively traversing route tree
*/
private $domainCollector = [];
public function _initialize()
{
defined('APPLICATION_ENV') || define('APPLICATION_ENV', $this->config['env']);
defined('APPLICATION_PATH') || define(
'APPLICATION_PATH',
Configuration::projectDir() . $this->config['app_path']
);
defined('LIBRARY_PATH') || define('LIBRARY_PATH', Configuration::projectDir() . $this->config['lib_path']);
// Ensure library/ is on include_path
set_include_path(
implode(
PATH_SEPARATOR,
[
LIBRARY_PATH,
get_include_path(),
]
)
);
require_once 'Zend/Loader/Autoloader.php';
\Zend_Loader_Autoloader::getInstance();
}
public function _before(TestInterface $test)
{
$this->client = new ZF1Connector();
\Zend_Session::$_unitTestEnabled = true;
try {
$this->bootstrap = new \Zend_Application(
$this->config['env'],
Configuration::projectDir() . $this->config['config']
);
} catch (\Exception $e) {
throw new ModuleException(__CLASS__, $e->getMessage());
}
$this->bootstrap->bootstrap();
$this->client->setBootstrap($this->bootstrap);
$db = $this->bootstrap->getBootstrap()->getResource('db');
if ($db instanceof \Zend_Db_Adapter_Abstract) {
$this->db = $db;
$this->db->getProfiler()->setEnabled(true);
$this->db->getProfiler()->clear();
}
}
public function _after(TestInterface $test)
{
$_SESSION = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
if ($this->bootstrap) {
$fc = $this->bootstrap->getBootstrap()->getResource('frontcontroller');
if ($fc) {
$fc->resetInstance();
}
}
\Zend_Layout::resetMvcInstance();
\Zend_Controller_Action_HelperBroker::resetHelpers();
\Zend_Session::$_unitTestEnabled = true;
\Zend_Registry::_unsetInstance();
$this->queries = 0;
$this->time = 0;
parent::_after($test);
}
/**
* @param $url
*/
protected function debugResponse($url)
{
parent::debugResponse($url);
$this->debugSection('Session', json_encode($_COOKIE));
if ($this->db) {
$profiler = $this->db->getProfiler();
$queries = $profiler->getTotalNumQueries() - $this->queries;
$time = $profiler->getTotalElapsedSecs() - $this->time;
$this->debugSection('Db', $queries . ' queries');
$this->debugSection('Time', round($time, 2) . ' secs taken');
$this->time = $profiler->getTotalElapsedSecs();
$this->queries = $profiler->getTotalNumQueries();
}
}
/**
* Opens web page using route name and parameters.
*
* ``` php
* <?php
* $I->amOnRoute('posts.create');
* $I->amOnRoute('posts.show', array('id' => 34));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function amOnRoute($routeName, array $params = [])
{
$router = $this->bootstrap->getBootstrap()->getResource('frontcontroller')->getRouter();
$url = $router->assemble($params, $routeName);
$this->amOnPage($url);
}
/**
* Checks that current url matches route.
*
* ``` php
* <?php
* $I->seeCurrentRouteIs('posts.index');
* $I->seeCurrentRouteIs('posts.show', ['id' => 8]));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function seeCurrentRouteIs($routeName, array $params = [])
{
$router = $this->bootstrap->getBootstrap()->getResource('frontcontroller')->getRouter();
$url = $router->assemble($params, $routeName);
$this->seeCurrentUrlEquals($url);
}
protected function getInternalDomains()
{
$router = $this->bootstrap->getBootstrap()->getResource('frontcontroller')->getRouter();
$this->domainCollector = [];
$this->addInternalDomainsFromRoutes($router->getRoutes());
return array_unique($this->domainCollector);
}
private function addInternalDomainsFromRoutes($routes)
{
foreach ($routes as $name => $route) {
try {
$route->assemble([]);
} catch (\Exception $e) {
}
if ($route instanceof Zend_Controller_Router_Route_Hostname) {
$this->addInternalDomain($route);
} elseif ($route instanceof Zend_Controller_Router_Route_Chain) {
$chainRoutes = ReflectionHelper::readPrivateProperty($route, '_routes');
$this->addInternalDomainsFromRoutes($chainRoutes);
}
}
}
private function addInternalDomain(Zend_Controller_Router_Route_Hostname $route)
{
$parts = ReflectionHelper::readPrivateProperty($route, '_parts');
foreach ($parts as &$part) {
if ($part === null) {
$part = '[^.]+';
}
}
$regex = implode('\.', $parts);
$this->domainCollector []= '/^' . $regex . '$/iu';
}
}

View File

@@ -0,0 +1,256 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Framework;
use Codeception\TestInterface;
use Codeception\Configuration;
use Codeception\Lib\Interfaces\DoctrineProvider;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\Util\ReflectionHelper;
use Zend\Console\Console;
use Zend\EventManager\StaticEventManager;
use Codeception\Lib\Connector\ZF2 as ZF2Connector;
/**
* This module allows you to run tests inside Zend Framework 2 and Zend Framework 3.
*
* File `init_autoloader` in project's root is required by Zend Framework 2.
* Uses `tests/application.config.php` config file by default.
*
* Note: services part and Doctrine integration is not compatible with ZF3 yet
*
* ## Status
*
* * Maintainer: **Naktibalda**
* * Stability: **stable**
*
* ## Config
*
* * config: relative path to config file (default: `tests/application.config.php`)
*
* ## Public Properties
*
* * application - instance of `\Zend\Mvc\ApplicationInterface`
* * db - instance of `\Zend\Db\Adapter\AdapterInterface`
* * client - BrowserKit client
*
* ## Parts
*
* * services - allows to use grabServiceFromContainer and addServiceToContainer with WebDriver or PhpBrowser modules.
*
* Usage example:
*
* ```yaml
* actor: AcceptanceTester
* modules:
* enabled:
* - ZF2:
* part: services
* - Doctrine2:
* depends: ZF2
* - WebDriver:
* url: http://your-url.com
* browser: phantomjs
* ```
*/
class ZF2 extends Framework implements DoctrineProvider, PartedModule
{
protected $config = [
'config' => 'tests/application.config.php',
];
/**
* @var \Zend\Mvc\ApplicationInterface
*/
public $application;
/**
* @var \Zend\Db\Adapter\AdapterInterface
*/
public $db;
/**
* @var \Codeception\Lib\Connector\ZF2
*/
public $client;
protected $applicationConfig;
protected $queries = 0;
protected $time = 0;
/**
* @var array Used to collect domains while recursively traversing route tree
*/
private $domainCollector = [];
public function _initialize()
{
$initAutoloaderFile = Configuration::projectDir() . 'init_autoloader.php';
if (file_exists($initAutoloaderFile)) {
require $initAutoloaderFile;
}
$this->applicationConfig = require Configuration::projectDir() . $this->config['config'];
if (isset($this->applicationConfig['module_listener_options']['config_cache_enabled'])) {
$this->applicationConfig['module_listener_options']['config_cache_enabled'] = false;
}
Console::overrideIsConsole(false);
//grabServiceFromContainer may need client in beforeClass hooks of modules or helpers
$this->client = new ZF2Connector();
$this->client->setApplicationConfig($this->applicationConfig);
}
public function _before(TestInterface $test)
{
$this->client = new ZF2Connector();
$this->client->setApplicationConfig($this->applicationConfig);
$_SERVER['REQUEST_URI'] = '';
}
public function _after(TestInterface $test)
{
$_SESSION = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
if (class_exists('Zend\EventManager\StaticEventManager')) {
// reset singleton (ZF2)
StaticEventManager::resetInstance();
}
$this->queries = 0;
$this->time = 0;
parent::_after($test);
}
public function _afterSuite()
{
unset($this->client);
}
public function _getEntityManager()
{
if (!$this->client) {
$this->client = new ZF2Connector();
$this->client->setApplicationConfig($this->applicationConfig);
}
return $this->grabServiceFromContainer('Doctrine\ORM\EntityManager');
}
/**
* Grabs a service from ZF2 container.
* Recommended to use for unit testing.
*
* ``` php
* <?php
* $em = $I->grabServiceFromContainer('Doctrine\ORM\EntityManager');
* ?>
* ```
*
* @param $service
* @return mixed
* @part services
*/
public function grabServiceFromContainer($service)
{
return $this->client->grabServiceFromContainer($service);
}
/**
* Adds service to ZF2 container
* @param string $name
* @param object $service
* @part services
*/
public function addServiceToContainer($name, $service)
{
$this->client->addServiceToContainer($name, $service);
}
/**
* Opens web page using route name and parameters.
*
* ``` php
* <?php
* $I->amOnRoute('posts.create');
* $I->amOnRoute('posts.show', array('id' => 34));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function amOnRoute($routeName, array $params = [])
{
$router = $this->client->grabServiceFromContainer('router');
$url = $router->assemble($params, ['name' => $routeName]);
$this->amOnPage($url);
}
/**
* Checks that current url matches route.
*
* ``` php
* <?php
* $I->seeCurrentRouteIs('posts.index');
* $I->seeCurrentRouteIs('posts.show', ['id' => 8]));
* ?>
* ```
*
* @param $routeName
* @param array $params
*/
public function seeCurrentRouteIs($routeName, array $params = [])
{
$router = $this->client->grabServiceFromContainer('router');
$url = $router->assemble($params, ['name' => $routeName]);
$this->seeCurrentUrlEquals($url);
}
protected function getInternalDomains()
{
/**
* @var Zend\Mvc\Router\Http\TreeRouteStack
*/
$router = $this->client->grabServiceFromContainer('router');
$this->domainCollector = [];
$this->addInternalDomainsFromRoutes($router->getRoutes());
return array_unique($this->domainCollector);
}
private function addInternalDomainsFromRoutes($routes)
{
foreach ($routes as $name => $route) {
if ($route instanceof \Zend\Mvc\Router\Http\Hostname || $route instanceof \Zend\Router\Http\Hostname) {
$this->addInternalDomain($route);
} elseif ($route instanceof \Zend\Mvc\Router\Http\Part || $route instanceof \Zend\Router\Http\Part) {
$parentRoute = ReflectionHelper::readPrivateProperty($route, 'route');
if ($parentRoute instanceof \Zend\Mvc\Router\Http\Hostname || $parentRoute instanceof \Zend\Mvc\Router\Http\Hostname) {
$this->addInternalDomain($parentRoute);
}
// this is necessary to instantiate child routes
try {
$route->assemble([], []);
} catch (\Exception $e) {
}
$this->addInternalDomainsFromRoutes($route->getRoutes());
}
}
}
private function addInternalDomain($route)
{
$regex = ReflectionHelper::readPrivateProperty($route, 'regex');
$this->domainCollector []= '/^' . $regex . '$/';
}
public function _parts()
{
return ['services'];
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Codeception\Module;
use Codeception\Lib\Framework;
use Codeception\TestInterface;
use Codeception\Configuration;
use Codeception\Lib\Connector\ZendExpressive as ZendExpressiveConnector;
use Codeception\Lib\Interfaces\DoctrineProvider;
/**
* This module allows you to run tests inside Zend Expressive.
*
* Uses `config/container.php` file by default.
*
* ## Status
*
* * Maintainer: **Naktibalda**
* * Stability: **alpha**
*
* ## Config
*
* * container: relative path to file which returns Container (default: `config/container.php`)
*
* ## API
*
* * application - instance of `\Zend\Expressive\Application`
* * container - instance of `\Interop\Container\ContainerInterface`
* * client - BrowserKit client
*
*/
class ZendExpressive extends Framework implements DoctrineProvider
{
protected $config = [
'container' => 'config/container.php',
];
/**
* @var \Codeception\Lib\Connector\ZendExpressive
*/
public $client;
/**
* @var \Interop\Container\ContainerInterface
*/
public $container;
/**
* @var \Zend\Expressive\Application
*/
public $application;
protected $responseCollector;
public function _initialize()
{
$cwd = getcwd();
$projectDir = Configuration::projectDir();
chdir($projectDir);
$this->container = require $projectDir . $this->config['container'];
$app = $this->container->get('Zend\Expressive\Application');
$pipelineFile = $projectDir . 'config/pipeline.php';
if (file_exists($pipelineFile)) {
require $pipelineFile;
}
$routesFile = $projectDir . 'config/routes.php';
if (file_exists($routesFile)) {
require $routesFile;
}
chdir($cwd);
$this->application = $app;
$this->initResponseCollector();
}
public function _before(TestInterface $test)
{
$this->client = new ZendExpressiveConnector();
$this->client->setApplication($this->application);
$this->client->setResponseCollector($this->responseCollector);
}
public function _after(TestInterface $test)
{
//Close the session, if any are open
if (session_status() == PHP_SESSION_ACTIVE) {
session_write_close();
}
parent::_after($test);
}
private function initResponseCollector()
{
/**
* @var Zend\Expressive\Emitter\EmitterStack
*/
$emitterStack = $this->application->getEmitter();
while (!$emitterStack->isEmpty()) {
$emitterStack->pop();
}
$this->responseCollector = new ZendExpressiveConnector\ResponseCollector;
$emitterStack->unshift($this->responseCollector);
}
public function _getEntityManager()
{
$service = 'Doctrine\ORM\EntityManager';
if (!$this->container->has($service)) {
throw new \PHPUnit\Framework\AssertionFailedError("Service $service is not available in container");
}
return $this->container->get('Doctrine\ORM\EntityManager');
}
}