1
0

ConfigurationTestGenerator.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * generates a config unit test class
  5. *
  6. * This generator is meant to test all possible configuration combinations
  7. * without having to write endless amounts of code manually.
  8. *
  9. * DANGER: Too many options/settings and too high max iteration setting may trigger
  10. * a fork bomb. Please save your work before executing this script.
  11. */
  12. include 'Bootstrap.php';
  13. $vrd = array('view', 'read', 'delete');
  14. $vcud = array('view', 'create', 'read', 'delete');
  15. new ConfigurationTestGenerator(array(
  16. 'main/discussion' => array(
  17. array(
  18. 'setting' => true,
  19. 'tests' => array(
  20. array(
  21. 'conditions' => array('steps' => $vrd),
  22. 'type' => 'RegExp',
  23. 'args' => array(
  24. '#<div[^>]*id="opendisc"[^>]*>#',
  25. '$content',
  26. 'outputs enabled discussion correctly'
  27. ),
  28. ), array(
  29. 'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
  30. 'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
  31. 'type' => 'Equals',
  32. 'args' => array(
  33. 1,
  34. '$response["status"]',
  35. 'when discussions are enabled, but invalid flag posted, fail to create paste'
  36. ),
  37. ), array(
  38. 'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
  39. 'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
  40. 'type' => 'False',
  41. 'args' => array(
  42. '$this->_model->exists(Helper::getPasteId())',
  43. 'when discussions are enabled, but invalid flag posted, paste is not created'
  44. ),
  45. ),
  46. ),
  47. 'affects' => $vcud
  48. ), array(
  49. 'setting' => false,
  50. 'tests' => array(
  51. array(
  52. 'type' => 'NotRegExp',
  53. 'args' => array(
  54. '#<div[^>]*id="opendisc"[^>]*>#',
  55. '$content',
  56. 'outputs disabled discussion correctly'
  57. ),
  58. ),
  59. ),
  60. 'affects' => $vrd
  61. ),
  62. ),
  63. 'main/opendiscussion' => array(
  64. array(
  65. 'setting' => true,
  66. 'tests' => array(
  67. array(
  68. 'conditions' => array('main/discussion' => true),
  69. 'type' => 'RegExp',
  70. 'args' => array(
  71. '#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
  72. '$content',
  73. 'outputs checked discussion correctly'
  74. ),
  75. ),
  76. ),
  77. 'affects' => $vrd
  78. ), array(
  79. 'setting' => false,
  80. 'tests' => array(
  81. array(
  82. 'conditions' => array('main/discussion' => true),
  83. 'type' => 'NotRegExp',
  84. 'args' => array(
  85. '#<input[^>]+id="opendiscussion"[^>]*checked="checked"[^>]*>#',
  86. '$content',
  87. 'outputs unchecked discussion correctly'
  88. ),
  89. ),
  90. ),
  91. 'affects' => $vrd
  92. ),
  93. ),
  94. 'main/burnafterreadingselected' => array(
  95. array(
  96. 'setting' => true,
  97. 'tests' => array(
  98. array(
  99. 'type' => 'RegExp',
  100. 'args' => array(
  101. '#<input[^>]+id="burnafterreading"[^>]*checked="checked"[^>]*>#',
  102. '$content',
  103. 'preselects burn after reading option',
  104. ),
  105. ),
  106. ),
  107. 'affects' => array('view'),
  108. ), array(
  109. 'setting' => false,
  110. 'tests' => array(
  111. array(
  112. 'type' => 'NotRegExp',
  113. 'args' => array(
  114. '#<input[^>]+id="burnafterreading"[^>]*checked="checked"[^>]*>#',
  115. '$content',
  116. 'burn after reading option is unchecked',
  117. ),
  118. ),
  119. ),
  120. 'affects' => array('view'),
  121. ),
  122. ),
  123. 'main/password' => array(
  124. array(
  125. 'setting' => true,
  126. 'tests' => array(
  127. array(
  128. 'type' => 'RegExp',
  129. 'args' => array(
  130. '#<div[^>]*id="password"[^>]*>#',
  131. '$content',
  132. 'outputs password input correctly'
  133. ),
  134. ),
  135. ),
  136. 'affects' => $vrd
  137. ), array(
  138. 'setting' => false,
  139. 'tests' => array(
  140. array(
  141. 'conditions' => array('main/discussion' => true),
  142. 'type' => 'NotRegExp',
  143. 'args' => array(
  144. '#<div[^>]*id="password"[^>]*>#',
  145. '$content',
  146. 'removes password input correctly'
  147. ),
  148. ),
  149. ),
  150. 'affects' => $vrd
  151. ),
  152. ),
  153. 'main/template' => array(
  154. array(
  155. 'setting' => 'page',
  156. 'tests' => array(
  157. array(
  158. 'type' => 'RegExp',
  159. 'args' => array(
  160. '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d+\.\d+"[^>]*/>#',
  161. '$content',
  162. 'outputs "page" stylesheet correctly',
  163. ),
  164. ), array(
  165. 'type' => 'NotRegExp',
  166. 'args' => array(
  167. '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap/bootstrap-\d[\d\.]+\d\.css"[^>]*/>#',
  168. '$content',
  169. 'removes "bootstrap" stylesheet correctly',
  170. ),
  171. ),
  172. ),
  173. 'affects' => $vrd,
  174. ), array(
  175. 'setting' => 'bootstrap',
  176. 'tests' => array(
  177. array(
  178. 'type' => 'NotRegExp',
  179. 'args' => array(
  180. '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/privatebin\.css\\?\d+\.\d+"[^>]*/>#',
  181. '$content',
  182. 'removes "page" stylesheet correctly',
  183. ),
  184. ), array(
  185. 'type' => 'RegExp',
  186. 'args' => array(
  187. '#<link[^>]+type="text/css"[^>]+rel="stylesheet"[^>]+href="css/bootstrap/bootstrap-\d[\d\.]+\d\.css"[^>]*/>#',
  188. '$content',
  189. 'outputs "bootstrap" stylesheet correctly',
  190. ),
  191. ),
  192. ),
  193. 'affects' => $vrd,
  194. ),
  195. ),
  196. 'main/sizelimit' => array(
  197. array(
  198. 'setting' => 10,
  199. 'tests' => array(
  200. array(
  201. 'conditions' => array('steps' => array('create'), 'traffic/limit' => 10),
  202. 'type' => 'Equals',
  203. 'args' => array(
  204. 1,
  205. '$response["status"]',
  206. 'when sizelimit limit exceeded, fail to create paste'
  207. ),
  208. ),
  209. ),
  210. 'affects' => array('create')
  211. ), array(
  212. 'setting' => 2097152,
  213. 'tests' => array(
  214. array(
  215. 'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
  216. 'settings' => array('sleep(3)'),
  217. 'type' => 'Equals',
  218. 'args' => array(
  219. 0,
  220. '$response["status"]',
  221. 'when sizelimit limit is not reached, successfully create paste'
  222. ),
  223. ), array(
  224. 'conditions' => array('steps' => array('create'), 'traffic/limit' => 0, 'main/burnafterreadingselected' => true),
  225. 'settings' => array('sleep(3)'),
  226. 'type' => 'True',
  227. 'args' => array(
  228. '$this->_model->exists($response["id"])',
  229. 'when sizelimit limit is not reached, paste exists after posting data'
  230. ),
  231. ),
  232. ),
  233. 'affects' => array('create')
  234. ),
  235. ),
  236. 'traffic/limit' => array(
  237. array(
  238. 'setting' => 0,
  239. 'tests' => array(
  240. array(
  241. 'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
  242. 'type' => 'Equals',
  243. 'args' => array(
  244. 0,
  245. '$response["status"]',
  246. 'when traffic limit is disabled, successfully create paste'
  247. ),
  248. ), array(
  249. 'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
  250. 'type' => 'True',
  251. 'args' => array(
  252. '$this->_model->exists($response["id"])',
  253. 'when traffic limit is disabled, paste exists after posting data'
  254. ),
  255. ),
  256. ),
  257. 'affects' => array('create')
  258. ), array(
  259. 'setting' => 10,
  260. 'tests' => array(
  261. array(
  262. 'conditions' => array('steps' => array('create')),
  263. 'type' => 'Equals',
  264. 'args' => array(
  265. 1,
  266. '$response["status"]',
  267. 'when traffic limit is on and we do not wait, fail to create paste'
  268. ),
  269. ),
  270. ),
  271. 'affects' => array('create')
  272. ), array(
  273. 'setting' => 2,
  274. 'tests' => array(
  275. array(
  276. 'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
  277. 'settings' => array('sleep(3)'),
  278. 'type' => 'Equals',
  279. 'args' => array(
  280. 0,
  281. '$response["status"]',
  282. 'when traffic limit is on and we wait, successfully create paste'
  283. ),
  284. ), array(
  285. 'conditions' => array('steps' => array('create'), 'main/sizelimit' => 2097152),
  286. 'settings' => array('sleep(3)'),
  287. 'type' => 'True',
  288. 'args' => array(
  289. '$this->_model->exists($response["id"])',
  290. 'when traffic limit is on and we wait, paste exists after posting data'
  291. ),
  292. ),
  293. ),
  294. 'affects' => array('create')
  295. ),
  296. ),
  297. ));
  298. class ConfigurationTestGenerator
  299. {
  300. /**
  301. * endless loop protection, since we're working with a recursive function,
  302. * creating factorial configurations
  303. * @var int
  304. */
  305. const MAX_ITERATIONS = 2000;
  306. /**
  307. * options to test
  308. * @var array
  309. */
  310. private $_options;
  311. /**
  312. * iteration count to guarantee timely end
  313. * @var int
  314. */
  315. private $_iterationCount = 0;
  316. /**
  317. * generated configurations
  318. * @var array
  319. */
  320. private $_configurations = array(
  321. array('options' => array(), 'tests' => array(), 'affects' => array())
  322. );
  323. /**
  324. * constructor, generates the configuration test
  325. * @param array $options
  326. */
  327. public function __construct($options)
  328. {
  329. $this->_options = $options;
  330. // generate all possible combinations of options: options^settings
  331. $this->_generateConfigurations();
  332. $this->_writeConfigurationTest();
  333. }
  334. /**
  335. * write configuration test file based on generated configuration array
  336. */
  337. private function _writeConfigurationTest()
  338. {
  339. $defaultOptions = parse_ini_file(CONF, true);
  340. $code = $this->_getHeader();
  341. foreach ($this->_configurations as $key => $conf) {
  342. $fullOptions = array_replace_recursive($defaultOptions, $conf['options']);
  343. $options = Helper::varExportMin($fullOptions, true);
  344. foreach ($conf['affects'] as $step) {
  345. $testCode = $preCode = array();
  346. foreach ($conf['tests'] as $tests) {
  347. foreach ($tests[0] as $test) {
  348. // skip if test does not affect this step
  349. if (!in_array($step, $tests[1])) {
  350. continue;
  351. }
  352. // skip if not all test conditions are met
  353. if (array_key_exists('conditions', $test)) {
  354. while (list($path, $setting) = each($test['conditions'])) {
  355. if ($path == 'steps' && !in_array($step, $setting)) {
  356. continue 2;
  357. } elseif ($path != 'steps') {
  358. list($section, $option) = explode('/', $path);
  359. if ($fullOptions[$section][$option] !== $setting) {
  360. continue 2;
  361. }
  362. }
  363. }
  364. }
  365. if (array_key_exists('settings', $test)) {
  366. foreach ($test['settings'] as $setting) {
  367. $preCode[$setting] = $setting;
  368. }
  369. }
  370. $args = array();
  371. foreach ($test['args'] as $arg) {
  372. if (is_string($arg) && strpos($arg, '$') === 0) {
  373. $args[] = $arg;
  374. } else {
  375. $args[] = Helper::varExportMin($arg, true);
  376. }
  377. }
  378. $testCode[] = array($test['type'], implode(', ', $args));
  379. }
  380. }
  381. $code .= $this->_getFunction(
  382. ucfirst($step), $key, $options, $preCode, $testCode
  383. );
  384. }
  385. }
  386. $code .= '}' . PHP_EOL;
  387. file_put_contents('ConfigurationCombinationsTest.php', $code);
  388. }
  389. /**
  390. * get header of configuration test file
  391. *
  392. * @return string
  393. */
  394. private function _getHeader()
  395. {
  396. return <<<'EOT'
  397. <?php
  398. /**
  399. * DO NOT EDIT: This file is generated automatically using configGenerator.php
  400. */
  401. use PrivateBin\PrivateBin;
  402. use PrivateBin\Data\Filesystem;
  403. use PrivateBin\Persistence\ServerSalt;
  404. use PrivateBin\Persistence\TrafficLimiter;
  405. class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase
  406. {
  407. private $_conf;
  408. private $_model;
  409. private $_path;
  410. public function setUp()
  411. {
  412. /* Setup Routine */
  413. Helper::confBackup();
  414. $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
  415. $this->_model = Filesystem::getInstance(array('dir' => $this->_path));
  416. ServerSalt::setPath($this->_path);
  417. TrafficLimiter::setPath($this->_path);
  418. $this->reset();
  419. }
  420. public function tearDown()
  421. {
  422. /* Tear Down Routine */
  423. Helper::confRestore();
  424. Helper::rmDir($this->_path);
  425. }
  426. public function reset($configuration = array())
  427. {
  428. $_POST = array();
  429. $_GET = array();
  430. $_SERVER = array();
  431. if ($this->_model->exists(Helper::getPasteId()))
  432. $this->_model->delete(Helper::getPasteId());
  433. $configuration['model_options']['dir'] = $this->_path;
  434. $configuration['traffic']['dir'] = $this->_path;
  435. $configuration['purge']['dir'] = $this->_path;
  436. Helper::createIniFile(CONF, $configuration);
  437. }
  438. EOT;
  439. }
  440. /**
  441. * get unit tests function block
  442. *
  443. * @param string $step
  444. * @param int $key
  445. * @param array $options
  446. * @param array $preCode
  447. * @param array $testCode
  448. * @return string
  449. */
  450. private function _getFunction($step, $key, &$options, $preCode, $testCode)
  451. {
  452. if (count($testCode) == 0) {
  453. echo "skipping creation of test$step$key, no valid tests found for configuration: $options". PHP_EOL;
  454. return '';
  455. }
  456. $preString = $testString = '';
  457. foreach ($preCode as $setting) {
  458. $preString .= " $setting;" . PHP_EOL;
  459. }
  460. foreach ($testCode as $test) {
  461. $type = $test[0];
  462. $args = $test[1];
  463. $testString .= " \$this->assert$type($args);" . PHP_EOL;
  464. }
  465. $code = <<<EOT
  466. /**
  467. * @runInSeparateProcess
  468. */
  469. public function test$step$key()
  470. {
  471. \$this->reset($options);
  472. EOT;
  473. // step specific initialization
  474. switch ($step) {
  475. case 'Create':
  476. $code .= PHP_EOL . <<<'EOT'
  477. $_POST = Helper::getPaste();
  478. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  479. $_SERVER['REQUEST_METHOD'] = 'POST';
  480. $_SERVER['REMOTE_ADDR'] = '::1';
  481. TrafficLimiter::canPass();
  482. EOT;
  483. break;
  484. case 'Read':
  485. $code .= PHP_EOL . <<<'EOT'
  486. $this->_model->create(Helper::getPasteId(), Helper::getPaste());
  487. $_SERVER['QUERY_STRING'] = Helper::getPasteId();
  488. EOT;
  489. break;
  490. case 'Delete':
  491. $code .= PHP_EOL . <<<'EOT'
  492. $this->_model->create(Helper::getPasteId(), Helper::getPaste());
  493. $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
  494. $_GET['pasteid'] = Helper::getPasteId();
  495. $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $this->_model->read(Helper::getPasteId())->meta->salt);
  496. EOT;
  497. break;
  498. }
  499. // all steps
  500. $code .= PHP_EOL . $preString;
  501. $code .= <<<'EOT'
  502. ob_start();
  503. new PrivateBin;
  504. $content = ob_get_contents();
  505. ob_end_clean();
  506. EOT;
  507. // step specific tests
  508. switch ($step) {
  509. case 'Create':
  510. $code .= <<<'EOT'
  511. $response = json_decode($content, true);
  512. EOT;
  513. break;
  514. case 'Read':
  515. $code .= <<<'EOT'
  516. $this->assertContains(
  517. htmlspecialchars(json_encode(Helper::getPaste()['data']), ENT_NOQUOTES),
  518. $content,
  519. 'outputs data correctly'
  520. );
  521. EOT;
  522. break;
  523. case 'Delete':
  524. $code .= <<<'EOT'
  525. $this->assertRegExp(
  526. '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
  527. $content,
  528. 'outputs deleted status correctly'
  529. );
  530. $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
  531. EOT;
  532. break;
  533. }
  534. return $code . PHP_EOL . PHP_EOL . $testString . ' }' . PHP_EOL . PHP_EOL;
  535. }
  536. /**
  537. * recursive function to generate configurations based on options
  538. *
  539. * @throws Exception
  540. * @return array
  541. */
  542. private function _generateConfigurations()
  543. {
  544. // recursive factorial function
  545. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  546. echo 'max iterations reached, stopping', PHP_EOL;
  547. return $this->_configurations;
  548. }
  549. echo "generateConfigurations: iteration $this->_iterationCount", PHP_EOL;
  550. $continue = list($path, $settings) = each($this->_options);
  551. if ($continue === false) {
  552. return $this->_configurations;
  553. }
  554. list($section, $option) = explode('/', $path);
  555. for ($c = 0, $max = count($this->_configurations); $c < $max; ++$c) {
  556. if (!array_key_exists($section, $this->_configurations[$c]['options'])) {
  557. $this->_configurations[$c]['options'][$section] = array();
  558. }
  559. if (count($settings) == 0) {
  560. throw new Exception("Check your \$options: option $option has no settings!");
  561. }
  562. // set the first setting in the original configuration
  563. $setting = current($settings);
  564. $this->_addSetting($this->_configurations[$c], $setting, $section, $option);
  565. // create clones for each of the other settings
  566. while ($setting = next($settings)) {
  567. $clone = $this->_configurations[$c];
  568. $this->_configurations[] = $this->_addSetting($clone, $setting, $section, $option);
  569. }
  570. reset($settings);
  571. }
  572. return $this->_generateConfigurations();
  573. }
  574. /**
  575. * add a setting to the given configuration
  576. *
  577. * @param array $configuration
  578. * @param array $setting
  579. * @param string $section
  580. * @param string $option
  581. * @throws Exception
  582. * @return array
  583. */
  584. private function _addSetting(&$configuration, &$setting, &$section, &$option)
  585. {
  586. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  587. echo 'max iterations reached, stopping', PHP_EOL;
  588. return $configuration;
  589. }
  590. echo "addSetting: iteration $this->_iterationCount", PHP_EOL;
  591. if (
  592. array_key_exists($option, $configuration['options'][$section]) &&
  593. $configuration['options'][$section][$option] === $setting['setting']
  594. ) {
  595. $val = Helper::varExportMin($setting['setting'], true);
  596. throw new Exception("Endless loop or error in options detected: option '$option' already exists with setting '$val' in one of the configurations!");
  597. }
  598. $configuration['options'][$section][$option] = $setting['setting'];
  599. $configuration['tests'][$option] = array($setting['tests'], $setting['affects']);
  600. foreach ($setting['affects'] as $affects) {
  601. if (!in_array($affects, $configuration['affects'])) {
  602. $configuration['affects'][] = $affects;
  603. }
  604. }
  605. return $configuration;
  606. }
  607. }