ConfigurationTestGenerator.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  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. $vd = array('view', 'delete');
  14. $vcd = array('view', 'create', 'delete');
  15. new ConfigurationTestGenerator(array(
  16. 'main/discussion' => array(
  17. array(
  18. 'setting' => true,
  19. 'tests' => array(
  20. array(
  21. 'conditions' => array('steps' => $vd),
  22. 'type' => 'RegExp',
  23. 'args' => array(
  24. '#<div[^>]*id="opendiscussionoption"[^>]*>#',
  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' => $vcd,
  48. ), array(
  49. 'setting' => false,
  50. 'tests' => array(
  51. array(
  52. 'type' => 'NotRegExp',
  53. 'args' => array(
  54. '#<div[^>]*id="opendiscussionoption"[^>]*>#',
  55. '$content',
  56. 'outputs disabled discussion correctly',
  57. ),
  58. ),
  59. ),
  60. 'affects' => $vd,
  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' => $vd,
  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' => $vd,
  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' => $vd,
  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' => $vd,
  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\.]+\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' => $vd,
  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\.]+\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' => $vd,
  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_SAMPLE, 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, $fullOptions['main']['discussion']
  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\Controller;
  402. use PrivateBin\Data\Filesystem;
  403. use PrivateBin\Persistence\ServerSalt;
  404. use PrivateBin\Persistence\TrafficLimiter;
  405. use PrivateBin\Request;
  406. class ConfigurationCombinationsTest extends PHPUnit_Framework_TestCase
  407. {
  408. private $_conf;
  409. private $_model;
  410. private $_path;
  411. public function setUp()
  412. {
  413. /* Setup Routine */
  414. Helper::confBackup();
  415. $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'privatebin_data';
  416. $this->_model = new Filesystem(array('dir' => $this->_path));
  417. $this->reset();
  418. }
  419. public function tearDown()
  420. {
  421. /* Tear Down Routine */
  422. unlink(CONF);
  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. Helper::createIniFile(CONF, $configuration);
  435. }
  436. EOT;
  437. }
  438. /**
  439. * get unit tests function block
  440. *
  441. * @param string $step
  442. * @param int $key
  443. * @param array $options
  444. * @param array $preCode
  445. * @param array $testCode
  446. * @return string
  447. */
  448. private function _getFunction($step, $key, &$options, $preCode, $testCode, $discussionEnabled)
  449. {
  450. if (count($testCode) == 0) {
  451. echo "skipping creation of test$step$key, no valid tests found for configuration: $options" . PHP_EOL;
  452. return '';
  453. }
  454. $preString = $testString = '';
  455. foreach ($preCode as $setting) {
  456. $preString .= " $setting;" . PHP_EOL;
  457. }
  458. foreach ($testCode as $test) {
  459. $type = $test[0];
  460. $args = $test[1];
  461. $testString .= " \$this->assert$type($args);" . PHP_EOL;
  462. }
  463. $code = <<<EOT
  464. /**
  465. * @runInSeparateProcess
  466. */
  467. public function test$step$key()
  468. {
  469. \$this->reset($options);
  470. EOT;
  471. // step specific initialization
  472. switch ($step) {
  473. case 'Create':
  474. if ($discussionEnabled) {
  475. $code .= PHP_EOL . <<<'EOT'
  476. $paste = Helper::getPasteJson();
  477. EOT;
  478. } else {
  479. $code .= PHP_EOL . <<<'EOT'
  480. $paste = json_decode(Helper::getPasteJson(), true);
  481. $paste['adata'][2] = 0;
  482. $paste = json_encode($paste);
  483. EOT;
  484. }
  485. $code .= PHP_EOL . <<<'EOT'
  486. $file = tempnam(sys_get_temp_dir(), 'FOO');
  487. file_put_contents($file, $paste);
  488. Request::setInputStream($file);
  489. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  490. $_SERVER['REQUEST_METHOD'] = 'POST';
  491. $_SERVER['REMOTE_ADDR'] = '::1';
  492. TrafficLimiter::canPass();
  493. EOT;
  494. break;
  495. case 'Read':
  496. $code .= PHP_EOL . <<<'EOT'
  497. $this->_model->create(Helper::getPasteId(), Helper::getPaste());
  498. $_SERVER['QUERY_STRING'] = Helper::getPasteId();
  499. $_GET[Helper::getPasteId()] = '';
  500. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  501. EOT;
  502. break;
  503. case 'Delete':
  504. $code .= PHP_EOL . <<<'EOT'
  505. $this->_model->create(Helper::getPasteId(), Helper::getPaste());
  506. $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists before deleting data');
  507. $_GET['pasteid'] = Helper::getPasteId();
  508. $_GET['deletetoken'] = hash_hmac('sha256', Helper::getPasteId(), $this->_model->read(Helper::getPasteId())['meta']['salt']);
  509. EOT;
  510. break;
  511. }
  512. // all steps
  513. $code .= PHP_EOL . $preString;
  514. $code .= <<<'EOT'
  515. ob_start();
  516. new Controller;
  517. $content = ob_get_contents();
  518. ob_end_clean();
  519. EOT;
  520. // step specific tests
  521. switch ($step) {
  522. case 'Create':
  523. $code .= <<<'EOT'
  524. $response = json_decode($content, true);
  525. EOT;
  526. break;
  527. case 'Read':
  528. $code .= <<<'EOT'
  529. $response = json_decode($content, true);
  530. $this->assertEquals(0, $response['status'], 'outputs success status');
  531. $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs id correctly');
  532. $this->assertEquals(Helper::getPaste()['data'], $response['data'], 'outputs data correctly');
  533. EOT;
  534. break;
  535. case 'Delete':
  536. $code .= <<<'EOT'
  537. $this->assertRegExp(
  538. '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted[^<]*</div>#s',
  539. $content,
  540. 'outputs deleted status correctly'
  541. );
  542. $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted');
  543. EOT;
  544. break;
  545. }
  546. return $code . PHP_EOL . PHP_EOL . $testString . ' }' . PHP_EOL . PHP_EOL;
  547. }
  548. /**
  549. * recursive function to generate configurations based on options
  550. *
  551. * @throws Exception
  552. * @return array
  553. */
  554. private function _generateConfigurations()
  555. {
  556. // recursive factorial function
  557. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  558. echo 'max iterations reached, stopping', PHP_EOL;
  559. return $this->_configurations;
  560. }
  561. echo "generateConfigurations: iteration $this->_iterationCount", PHP_EOL;
  562. $continue = list($path, $settings) = each($this->_options);
  563. if ($continue === false) {
  564. return $this->_configurations;
  565. }
  566. list($section, $option) = explode('/', $path);
  567. for ($c = 0, $max = count($this->_configurations); $c < $max; ++$c) {
  568. if (!array_key_exists($section, $this->_configurations[$c]['options'])) {
  569. $this->_configurations[$c]['options'][$section] = array();
  570. }
  571. if (count($settings) == 0) {
  572. throw new Exception("Check your \$options: option $option has no settings!");
  573. }
  574. // set the first setting in the original configuration
  575. $setting = current($settings);
  576. $this->_addSetting($this->_configurations[$c], $setting, $section, $option);
  577. // create clones for each of the other settings
  578. while ($setting = next($settings)) {
  579. $clone = $this->_configurations[$c];
  580. $this->_configurations[] = $this->_addSetting($clone, $setting, $section, $option);
  581. }
  582. reset($settings);
  583. }
  584. return $this->_generateConfigurations();
  585. }
  586. /**
  587. * add a setting to the given configuration
  588. *
  589. * @param array $configuration
  590. * @param array $setting
  591. * @param string $section
  592. * @param string $option
  593. * @throws Exception
  594. * @return array
  595. */
  596. private function _addSetting(&$configuration, &$setting, &$section, &$option)
  597. {
  598. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  599. echo 'max iterations reached, stopping', PHP_EOL;
  600. return $configuration;
  601. }
  602. echo "addSetting: iteration $this->_iterationCount", PHP_EOL;
  603. if (
  604. array_key_exists($option, $configuration['options'][$section]) &&
  605. $configuration['options'][$section][$option] === $setting['setting']
  606. ) {
  607. $val = Helper::varExportMin($setting['setting'], true);
  608. throw new Exception("Endless loop or error in options detected: option '$option' already exists with setting '$val' in one of the configurations!");
  609. }
  610. $configuration['options'][$section][$option] = $setting['setting'];
  611. $configuration['tests'][$option] = array($setting['tests'], $setting['affects']);
  612. foreach ($setting['affects'] as $affects) {
  613. if (!in_array($affects, $configuration['affects'])) {
  614. $configuration['affects'][] = $affects;
  615. }
  616. }
  617. return $configuration;
  618. }
  619. }