ConfigurationTestGenerator.php 24 KB

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