#!/usr/bin/env php array( array( 'setting' => true, 'tests' => array( array( 'conditions' => array('steps' => $vd), 'type' => 'MatchesRegularExpression', 'args' => array( '#]*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( '#]*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( '#]+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( '#]+id="opendiscussion"[^>]*checked="checked"[^>]*>#', '$content', 'outputs unchecked discussion correctly', ), ), ), 'affects' => $vd, ), ), 'main/burnafterreadingselected' => array( array( 'setting' => true, 'tests' => array( array( 'type' => 'MatchesRegularExpression', 'args' => array( '#]+id="burnafterreading"[^>]*checked="checked"[^>]*>#', '$content', 'preselects burn after reading option', ), ), ), 'affects' => array('view'), ), array( 'setting' => false, 'tests' => array( array( 'type' => 'DoesNotMatchRegularExpression', 'args' => array( '#]+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( '#]*id="password"[^>]*>#', '$content', 'outputs password input correctly', ), ), ), 'affects' => $vd, ), array( 'setting' => false, 'tests' => array( array( 'conditions' => array('main/discussion' => true), 'type' => 'DoesNotMatchRegularExpression', 'args' => array( '#]*id="password"[^>]*>#', '$content', 'removes password input correctly', ), ), ), 'affects' => $vd, ), ), 'main/template' => array( array( 'setting' => 'page', 'tests' => array( array( 'type' => 'MatchesRegularExpression', 'args' => array( '#]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#', '$content', 'outputs "page" stylesheet correctly', ), ), array( 'type' => 'DoesNotMatchRegularExpression', 'args' => array( '#]+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( '#]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d[\d\.]+\d+"[^>]*/>#', '$content', 'removes "page" stylesheet correctly', ), ), array( 'type' => 'MatchesRegularExpression', 'args' => array( '#]+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' _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 = <<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( '#]*id="status"[^>]*>.*Paste was properly deleted[^<]*#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; } }