8c1310c334
having had to re-do all of these steps on a new environment, I noticed some inconsistencies and updated the doc: - moved unit testing doc into common doc folder, so it is easier to find - removed no longer supported Janitor reference - removed note regarding generated test duration, current version takes less than a minute - addressed each function removal & phpunit deprecations
655 lines
24 KiB
PHP
Executable File
655 lines
24 KiB
PHP
Executable File
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* generates a config unit test class
|
|
*
|
|
* This generator is meant to test all possible configuration combinations
|
|
* without having to write endless amounts of code manually.
|
|
*
|
|
* DANGER: Too many options/settings and too high max iteration setting may trigger
|
|
* a fork bomb. Please save your work before executing this script.
|
|
*/
|
|
|
|
define('PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
|
|
include PATH . 'tst' . DIRECTORY_SEPARATOR . 'Bootstrap.php';
|
|
|
|
$vd = array('view', 'delete');
|
|
$vcd = array('view', 'create', 'delete');
|
|
|
|
new ConfigurationTestGenerator(array(
|
|
'main/discussion' => array(
|
|
array(
|
|
'setting' => true,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => $vd),
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<div[^>]*id="opendiscussionoption"[^>]*>#',
|
|
'$content',
|
|
'outputs enabled discussion correctly',
|
|
),
|
|
), array(
|
|
'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
|
|
'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
1,
|
|
'$response["status"]',
|
|
'when discussions are enabled, but invalid flag posted, fail to create paste',
|
|
),
|
|
), array(
|
|
'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
|
|
'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
|
|
'type' => 'False',
|
|
'args' => array(
|
|
'$this->_model->exists(Helper::getPasteId())',
|
|
'when discussions are enabled, but invalid flag posted, paste is not created',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vcd,
|
|
), array(
|
|
'setting' => false,
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<div[^>]*id="opendiscussionoption"[^>]*>#',
|
|
'$content',
|
|
'outputs disabled discussion correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
),
|
|
),
|
|
'main/opendiscussion' => array(
|
|
array(
|
|
'setting' => true,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('main/discussion' => true),
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
|
|
'$content',
|
|
'outputs checked discussion correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
), array(
|
|
'setting' => false,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('main/discussion' => true),
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
|
|
'$content',
|
|
'outputs unchecked discussion correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
),
|
|
),
|
|
'main/burnafterreadingselected' => array(
|
|
array(
|
|
'setting' => true,
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<input[^>]+id="burnafterreading"[^>]*checked="checked"[^>]*>#',
|
|
'$content',
|
|
'preselects burn after reading option',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('view'),
|
|
), array(
|
|
'setting' => false,
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<input[^>]+id="burnafterreading"[^>]*checked="checked"[^>]*>#',
|
|
'$content',
|
|
'burn after reading option is unchecked',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('view'),
|
|
),
|
|
),
|
|
'main/password' => array(
|
|
array(
|
|
'setting' => true,
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<div[^>]*id="password"[^>]*>#',
|
|
'$content',
|
|
'outputs password input correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
), array(
|
|
'setting' => false,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('main/discussion' => true),
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<div[^>]*id="password"[^>]*>#',
|
|
'$content',
|
|
'removes password input correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
),
|
|
),
|
|
'main/template' => array(
|
|
array(
|
|
'setting' => 'page',
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#',
|
|
'$content',
|
|
'outputs "page" stylesheet correctly',
|
|
),
|
|
), array(
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap/bootstrap-\d[\d\.]+\d\.css"[^>]*/>#',
|
|
'$content',
|
|
'removes "bootstrap" stylesheet correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
), array(
|
|
'setting' => 'bootstrap',
|
|
'tests' => array(
|
|
array(
|
|
'type' => 'DoesNotMatchRegularExpression',
|
|
'args' => array(
|
|
'#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#',
|
|
'$content',
|
|
'removes "page" stylesheet correctly',
|
|
),
|
|
), array(
|
|
'type' => 'MatchesRegularExpression',
|
|
'args' => array(
|
|
'#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap/bootstrap-\d[\d\.]+\d\.css"[^>]*/>#',
|
|
'$content',
|
|
'outputs "bootstrap" stylesheet correctly',
|
|
),
|
|
),
|
|
),
|
|
'affects' => $vd,
|
|
),
|
|
),
|
|
'main/sizelimit' => array(
|
|
array(
|
|
'setting' => 10,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
1,
|
|
'$response["status"]',
|
|
'when sizelimit limit exceeded, fail to create paste',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('create'),
|
|
), array(
|
|
'setting' => 2097152,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
|
|
'settings' => array('sleep(3)'),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
0,
|
|
'$response["status"]',
|
|
'when sizelimit limit is not reached, successfully create paste',
|
|
),
|
|
), array(
|
|
'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
|
|
'settings' => array('sleep(3)'),
|
|
'type' => 'True',
|
|
'args' => array(
|
|
'$this->_model->exists($response["id"])',
|
|
'when sizelimit limit is not reached, paste exists after posting data',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('create'),
|
|
),
|
|
),
|
|
'traffic/limit' => array(
|
|
array(
|
|
'setting' => 0,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
0,
|
|
'$response["status"]',
|
|
'when traffic limit is disabled, successfully create paste',
|
|
),
|
|
), array(
|
|
'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
|
|
'type' => 'True',
|
|
'args' => array(
|
|
'$this->_model->exists($response["id"])',
|
|
'when traffic limit is disabled, paste exists after posting data',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('create'),
|
|
), array(
|
|
'setting' => 10,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => array('create')),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
1,
|
|
'$response["status"]',
|
|
'when traffic limit is on and we do not wait, fail to create paste',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('create'),
|
|
), array(
|
|
'setting' => 2,
|
|
'tests' => array(
|
|
array(
|
|
'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
|
|
'settings' => array('sleep(3)'),
|
|
'type' => 'Equals',
|
|
'args' => array(
|
|
0,
|
|
'$response["status"]',
|
|
'when traffic limit is on and we wait, successfully create paste',
|
|
),
|
|
), array(
|
|
'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
|
|
'settings' => array('sleep(3)'),
|
|
'type' => 'True',
|
|
'args' => array(
|
|
'$this->_model->exists($response["id"])',
|
|
'when traffic limit is on and we wait, paste exists after posting data',
|
|
),
|
|
),
|
|
),
|
|
'affects' => array('create'),
|
|
),
|
|
),
|
|
));
|
|
|
|
class ConfigurationTestGenerator
|
|
{
|
|
/**
|
|
* endless loop protection, since we're working with a recursive function,
|
|
* creating factorial configurations
|
|
* @var int
|
|
*/
|
|
const MAX_ITERATIONS = 2000;
|
|
|
|
/**
|
|
* options to test
|
|
* @var array
|
|
*/
|
|
private $_options;
|
|
|
|
/**
|
|
* iteration count to guarantee timely end
|
|
* @var int
|
|
*/
|
|
private $_iterationCount = 0;
|
|
|
|
/**
|
|
* generated configurations
|
|
* @var array
|
|
*/
|
|
private $_configurations = array(
|
|
array('options' => array(), 'tests' => array(), 'affects' => array()),
|
|
);
|
|
|
|
/**
|
|
* constructor, generates the configuration test
|
|
* @param array $options
|
|
*/
|
|
public function __construct($options)
|
|
{
|
|
$this->_options = $options;
|
|
// generate all possible combinations of options: options^settings
|
|
$this->_generateConfigurations();
|
|
$this->_writeConfigurationTest();
|
|
}
|
|
|
|
/**
|
|
* write configuration test file based on generated configuration array
|
|
*/
|
|
private function _writeConfigurationTest()
|
|
{
|
|
$defaultOptions = parse_ini_file(CONF_SAMPLE, true);
|
|
$code = $this->_getHeader();
|
|
foreach ($this->_configurations as $key => $conf) {
|
|
$fullOptions = array_replace_recursive($defaultOptions, $conf['options']);
|
|
$options = Helper::varExportMin($fullOptions, true);
|
|
foreach ($conf['affects'] as $step) {
|
|
$testCode = $preCode = array();
|
|
foreach ($conf['tests'] as $tests) {
|
|
foreach ($tests[0] as $test) {
|
|
// skip if test does not affect this step
|
|
if (!in_array($step, $tests[1])) {
|
|
continue;
|
|
}
|
|
// skip if not all test conditions are met
|
|
if (array_key_exists('conditions', $test)) {
|
|
foreach ($test['conditions'] as $path => $setting) {
|
|
if ($path == 'steps' && !in_array($step, $setting)) {
|
|
continue 2;
|
|
} elseif ($path != 'steps') {
|
|
list($section, $option) = explode('/', $path);
|
|
if ($fullOptions[$section][$option] !== $setting) {
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (array_key_exists('settings', $test)) {
|
|
foreach ($test['settings'] as $setting) {
|
|
$preCode[$setting] = $setting;
|
|
}
|
|
}
|
|
$args = array();
|
|
foreach ($test['args'] as $arg) {
|
|
if (is_string($arg) && strpos($arg, '$') === 0) {
|
|
$args[] = $arg;
|
|
} else {
|
|
$args[] = Helper::varExportMin($arg, true);
|
|
}
|
|
}
|
|
$testCode[] = array($test['type'], implode(', ', $args));
|
|
}
|
|
}
|
|
$code .= $this->_getFunction(
|
|
ucfirst($step), $key, $options, $preCode, $testCode, $fullOptions['main']['discussion']
|
|
);
|
|
}
|
|
}
|
|
$code .= '}' . PHP_EOL;
|
|
file_put_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'tst' . DIRECTORY_SEPARATOR . 'ConfigurationCombinationsTest.php', $code);
|
|
}
|
|
|
|
/**
|
|
* get header of configuration test file
|
|
*
|
|
* @return string
|
|
*/
|
|
private function _getHeader()
|
|
{
|
|
return <<<'EOT'
|
|
<?php
|
|
/**
|
|
* DO NOT EDIT: This file is generated automatically using configGenerator.php
|
|
*/
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use PrivateBin\Controller;
|
|
use PrivateBin\Data\Filesystem;
|
|
use PrivateBin\Persistence\ServerSalt;
|
|
use PrivateBin\Persistence\TrafficLimiter;
|
|
use PrivateBin\Request;
|
|
|
|
class ConfigurationCombinationsTest extends TestCase
|
|
{
|
|
private $_conf;
|
|
|
|
private $_model;
|
|
|
|
private $_path;
|
|
|
|
public function setUp(): void
|
|
{
|
|
/* Setup Routine */
|
|
Helper::confBackup();
|
|
$this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
|
|
$this->_model = new Filesystem(array('dir' => $this->_path));
|
|
ServerSalt::setStore($this->_model);
|
|
TrafficLimiter::setStore($this->_model);
|
|
$this->reset();
|
|
}
|
|
|
|
public function tearDown(): void
|
|
{
|
|
/* Tear Down Routine */
|
|
unlink(CONF);
|
|
Helper::confRestore();
|
|
Helper::rmDir($this->_path);
|
|
}
|
|
|
|
public function reset($configuration = array())
|
|
{
|
|
$_POST = array();
|
|
$_GET = array();
|
|
$_SERVER = array();
|
|
if ($this->_model->exists(Helper::getPasteId()))
|
|
$this->_model->delete(Helper::getPasteId());
|
|
$configuration['model_options']['dir'] = $this->_path;
|
|
Helper::createIniFile(CONF, $configuration);
|
|
}
|
|
|
|
|
|
EOT;
|
|
}
|
|
|
|
/**
|
|
* get unit tests function block
|
|
*
|
|
* @param string $step
|
|
* @param int $key
|
|
* @param array $options
|
|
* @param array $preCode
|
|
* @param array $testCode
|
|
* @return string
|
|
*/
|
|
private function _getFunction($step, $key, &$options, $preCode, $testCode, $discussionEnabled)
|
|
{
|
|
if (count($testCode) == 0) {
|
|
echo "skipping creation of test$step$key, no valid tests found for configuration: $options" . PHP_EOL;
|
|
return '';
|
|
}
|
|
|
|
$preString = $testString = '';
|
|
foreach ($preCode as $setting) {
|
|
$preString .= " $setting;" . PHP_EOL;
|
|
}
|
|
foreach ($testCode as $test) {
|
|
$type = $test[0];
|
|
$args = $test[1];
|
|
$testString .= " \$this->assert$type($args);" . PHP_EOL;
|
|
}
|
|
$code = <<<EOT
|
|
/**
|
|
* @runInSeparateProcess
|
|
*/
|
|
public function test$step$key()
|
|
{
|
|
\$this->reset($options);
|
|
EOT;
|
|
|
|
// step specific initialization
|
|
switch ($step) {
|
|
case 'Create':
|
|
if ($discussionEnabled) {
|
|
$code .= PHP_EOL . <<<'EOT'
|
|
$paste = Helper::getPasteJson();
|
|
EOT;
|
|
} else {
|
|
$code .= PHP_EOL . <<<'EOT'
|
|
$paste = json_decode(Helper::getPasteJson(), true);
|
|
$paste['adata'][2] = 0;
|
|
$paste = json_encode($paste);
|
|
EOT;
|
|
}
|
|
$code .= PHP_EOL . <<<'EOT'
|
|
$file = tempnam(sys_get_temp_dir(), 'FOO');
|
|
file_put_contents($file, $paste);
|
|
Request::setInputStream($file);
|
|
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
|
$_SERVER['REQUEST_METHOD'] = 'POST';
|
|
$_SERVER['REMOTE_ADDR'] = '::1';
|
|
TrafficLimiter::canPass();
|
|
EOT;
|
|
break;
|
|
case 'Read':
|
|
$code .= PHP_EOL . <<<'EOT'
|
|
$this->_model->create(Helper::getPasteId(), Helper::getPaste());
|
|
$_SERVER['QUERY_STRING'] = Helper::getPasteId();
|
|
$_GET[Helper::getPasteId()] = '';
|
|
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
|
|
EOT;
|
|
break;
|
|
case 'Delete':
|
|
$code .= PHP_EOL . <<<'EOT'
|
|
$this->_model->create(Helper::getPasteId(), Helper::getPaste());
|
|
$this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
|
|
$_GET['pasteid'] = Helper::getPasteId();
|
|
$_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $this->_model->read(Helper::getPasteId())['meta']['salt']);
|
|
EOT;
|
|
break;
|
|
}
|
|
|
|
// all steps
|
|
$code .= PHP_EOL . $preString;
|
|
$code .= <<<'EOT'
|
|
ob_start();
|
|
new Controller;
|
|
$content = ob_get_contents();
|
|
ob_end_clean();
|
|
EOT;
|
|
|
|
// step specific tests
|
|
switch ($step) {
|
|
case 'Create':
|
|
$code .= <<<'EOT'
|
|
|
|
$response = json_decode($content, true);
|
|
EOT;
|
|
break;
|
|
case 'Read':
|
|
$code .= <<<'EOT'
|
|
|
|
$response = json_decode($content, true);
|
|
$this->assertEquals(0, $response['status'], 'outputs success status');
|
|
$this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs id correctly');
|
|
$this->assertEquals(Helper::getPaste()['data'], $response['data'], 'outputs data correctly');
|
|
EOT;
|
|
break;
|
|
case 'Delete':
|
|
$code .= <<<'EOT'
|
|
|
|
$this->assertMatchesRegularExpression(
|
|
'#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
|
|
$content,
|
|
'outputs deleted status correctly'
|
|
);
|
|
$this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
|
|
EOT;
|
|
break;
|
|
}
|
|
return $code . PHP_EOL . PHP_EOL . $testString . ' }' . PHP_EOL . PHP_EOL;
|
|
}
|
|
|
|
/**
|
|
* recursive function to generate configurations based on options
|
|
*
|
|
* @throws Exception
|
|
* @return array
|
|
*/
|
|
private function _generateConfigurations()
|
|
{
|
|
// recursive factorial function
|
|
if (++$this->_iterationCount > self::MAX_ITERATIONS) {
|
|
echo 'max iterations reached, stopping', PHP_EOL;
|
|
return $this->_configurations;
|
|
}
|
|
echo "generateConfigurations: iteration $this->_iterationCount", PHP_EOL;
|
|
$path = key($this->_options);
|
|
$settings = current($this->_options);
|
|
if (next($this->_options) === false) {
|
|
return $this->_configurations;
|
|
}
|
|
list($section, $option) = explode('/', $path);
|
|
for ($c = 0, $max = count($this->_configurations); $c < $max; ++$c) {
|
|
if (!array_key_exists($section, $this->_configurations[$c]['options'])) {
|
|
$this->_configurations[$c]['options'][$section] = array();
|
|
}
|
|
if (count($settings) == 0) {
|
|
throw new Exception("Check your \$options: option $option has no settings!");
|
|
}
|
|
// set the first setting in the original configuration
|
|
$setting = current($settings);
|
|
$this->_addSetting($this->_configurations[$c], $setting, $section, $option);
|
|
|
|
// create clones for each of the other settings
|
|
while ($setting = next($settings)) {
|
|
$clone = $this->_configurations[$c];
|
|
$this->_configurations[] = $this->_addSetting($clone, $setting, $section, $option);
|
|
}
|
|
reset($settings);
|
|
}
|
|
return $this->_generateConfigurations();
|
|
}
|
|
|
|
/**
|
|
* add a setting to the given configuration
|
|
*
|
|
* @param array $configuration
|
|
* @param array $setting
|
|
* @param string $section
|
|
* @param string $option
|
|
* @throws Exception
|
|
* @return array
|
|
*/
|
|
private function _addSetting(&$configuration, &$setting, &$section, &$option)
|
|
{
|
|
if (++$this->_iterationCount > self::MAX_ITERATIONS) {
|
|
echo 'max iterations reached, stopping', PHP_EOL;
|
|
return $configuration;
|
|
}
|
|
echo "addSetting: iteration $this->_iterationCount", PHP_EOL;
|
|
if (
|
|
array_key_exists($option, $configuration['options'][$section]) &&
|
|
$configuration['options'][$section][$option] === $setting['setting']
|
|
) {
|
|
$val = Helper::varExportMin($setting['setting'], true);
|
|
throw new Exception("Endless loop or error in options detected: option '$option' already exists with setting '$val' in one of the configurations!");
|
|
}
|
|
$configuration['options'][$section][$option] = $setting['setting'];
|
|
$configuration['tests'][$option] = array($setting['tests'], $setting['affects']);
|
|
foreach ($setting['affects'] as $affects) {
|
|
if (!in_array($affects, $configuration['affects'])) {
|
|
$configuration['affects'][] = $affects;
|
|
}
|
|
}
|
|
return $configuration;
|
|
}
|
|
}
|