configGenerator.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  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. $this->_options = $options;
  385. // generate all possible combinations of options: options^settings
  386. $this->_generateConfigurations();
  387. $this->_writeConfigurationTest();
  388. }
  389. /**
  390. * write configuration test file based on generated configuration array
  391. */
  392. private function _writeConfigurationTest()
  393. {
  394. $defaultOptions = parse_ini_file(CONF, true);
  395. $code = $this->_getHeader();
  396. foreach ($this->_configurations as $key => $conf) {
  397. $fullOptions = array_replace_recursive($defaultOptions, $conf['options']);
  398. $options = helper::var_export_min($fullOptions, true);
  399. foreach ($conf['affects'] as $step) {
  400. $testCode = $preCode = array();
  401. foreach ($conf['tests'] as $tests) {
  402. foreach ($tests[0] as $test) {
  403. // skip if test does not affect this step
  404. if (!in_array($step, $tests[1])) {
  405. continue;
  406. }
  407. // skip if not all test conditions are met
  408. if (array_key_exists('conditions', $test)) {
  409. while (list($path, $setting) = each($test['conditions'])) {
  410. if ($path == 'steps' && !in_array($step, $setting)) {
  411. continue 2;
  412. } elseif($path != 'steps') {
  413. list($section, $option) = explode('/', $path);
  414. if ($fullOptions[$section][$option] !== $setting) {
  415. continue 2;
  416. }
  417. }
  418. }
  419. }
  420. if (array_key_exists('settings', $test)) {
  421. foreach ($test['settings'] as $setting) {
  422. $preCode[$setting] = $setting;
  423. }
  424. }
  425. $args = array();
  426. foreach ($test['args'] as $arg) {
  427. if (is_string($arg) && strpos($arg, '$') === 0) {
  428. $args[] = $arg;
  429. } else {
  430. $args[] = helper::var_export_min($arg, true);
  431. }
  432. }
  433. $testCode[] = array($test['type'], implode(', ', $args));
  434. }
  435. }
  436. $code .= $this->_getFunction(
  437. ucfirst($step), $key, $options, $preCode, $testCode
  438. );
  439. }
  440. }
  441. $code .= '}' . PHP_EOL;
  442. file_put_contents('configurationCombinations.php', $code);
  443. }
  444. /**
  445. * get header of configuration test file
  446. *
  447. * @return string
  448. */
  449. private function _getHeader()
  450. {
  451. return <<<'EOT'
  452. <?php
  453. /**
  454. * DO NOT EDIT: This file is generated automatically using configGenerator.php
  455. */
  456. class configurationCombinationsTest extends PHPUnit_Framework_TestCase
  457. {
  458. private $_model;
  459. private $_conf;
  460. public function setUp()
  461. {
  462. /* Setup Routine */
  463. helper::confBackup();
  464. $this->_model = privatebin_data::getInstance(array('dir' => PATH . 'data'));
  465. serversalt::setPath(PATH . 'data');
  466. $this->reset();
  467. }
  468. public function tearDown()
  469. {
  470. /* Tear Down Routine */
  471. helper::confRestore();
  472. }
  473. public function reset($configuration = array())
  474. {
  475. $_POST = array();
  476. $_GET = array();
  477. $_SERVER = array();
  478. if ($this->_model->exists(helper::getPasteId()))
  479. $this->_model->delete(helper::getPasteId());
  480. helper::createIniFile(CONF, $configuration);
  481. }
  482. EOT;
  483. }
  484. /**
  485. * get unit tests function block
  486. *
  487. * @param string $step
  488. * @param int $key
  489. * @param array $options
  490. * @param array $preCode
  491. * @param array $testCode
  492. * @return string
  493. */
  494. private function _getFunction($step, $key, &$options, $preCode, $testCode)
  495. {
  496. if (count($testCode) == 0) {
  497. echo "skipping creation of test$step$key, no valid tests found for configuration: $options". PHP_EOL;
  498. return '';
  499. }
  500. $preString = $testString = '';
  501. foreach ($preCode as $setting) {
  502. $preString .= " $setting;" . PHP_EOL;
  503. }
  504. foreach ($testCode as $test) {
  505. $type = $test[0];
  506. $args = $test[1];
  507. $testString .= " \$this->assert$type($args);" . PHP_EOL;
  508. }
  509. $code = <<<EOT
  510. /**
  511. * @runInSeparateProcess
  512. */
  513. public function test$step$key()
  514. {
  515. \$this->reset($options);
  516. EOT;
  517. // step specific initialization
  518. switch ($step) {
  519. case 'Create':
  520. $code .= PHP_EOL . <<<'EOT'
  521. $_POST = helper::getPaste();
  522. $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
  523. $_SERVER['REQUEST_METHOD'] = 'POST';
  524. $_SERVER['REMOTE_ADDR'] = '::1';
  525. EOT;
  526. break;
  527. case 'Read':
  528. $code .= PHP_EOL . <<<'EOT'
  529. $this->_model->create(helper::getPasteId(), helper::getPaste());
  530. $_SERVER['QUERY_STRING'] = helper::getPasteId();
  531. EOT;
  532. break;
  533. case 'Delete':
  534. $code .= PHP_EOL . <<<'EOT'
  535. $this->_model->create(helper::getPasteId(), helper::getPaste());
  536. $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
  537. $_GET['pasteid'] = helper::getPasteId();
  538. $_GET['deletetoken'] = hash_hmac('sha1', helper::getPasteId(), serversalt::get());
  539. EOT;
  540. break;
  541. }
  542. // all steps
  543. $code .= PHP_EOL . $preString;
  544. $code .= <<<'EOT'
  545. ob_start();
  546. new privatebin;
  547. $content = ob_get_contents();
  548. EOT;
  549. // step specific tests
  550. switch ($step) {
  551. case 'Create':
  552. $code .= <<<'EOT'
  553. $response = json_decode($content, true);
  554. EOT;
  555. break;
  556. case 'Read':
  557. $code .= <<<'EOT'
  558. $this->assertContains(
  559. '<div id="cipherdata" class="hidden">' .
  560. htmlspecialchars(helper::getPasteAsJson(), ENT_NOQUOTES) .
  561. '</div>',
  562. $content,
  563. 'outputs data correctly'
  564. );
  565. EOT;
  566. break;
  567. case 'Delete':
  568. $code .= <<<'EOT'
  569. $this->assertRegExp(
  570. '#<div[^>]*id="status"[^>]*>.*Paste was properly deleted</div>#',
  571. $content,
  572. 'outputs deleted status correctly'
  573. );
  574. $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
  575. EOT;
  576. break;
  577. }
  578. return $code . PHP_EOL . PHP_EOL . $testString . ' }' . PHP_EOL . PHP_EOL;
  579. }
  580. /**
  581. * recursive function to generate configurations based on options
  582. *
  583. * @throws Exception
  584. * @return array
  585. */
  586. private function _generateConfigurations()
  587. {
  588. // recursive factorial function
  589. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  590. echo 'max iterations reached, stopping', PHP_EOL;
  591. return $this->_configurations;
  592. }
  593. echo "generateConfigurations: iteration $this->_iterationCount", PHP_EOL;
  594. $continue = list($path, $settings) = each($this->_options);
  595. if ($continue === false) {
  596. return $this->_configurations;
  597. }
  598. list($section, $option) = explode('/', $path);
  599. for ($c = 0, $max = count($this->_configurations); $c < $max; ++$c) {
  600. if (!array_key_exists($section, $this->_configurations[$c]['options'])) {
  601. $this->_configurations[$c]['options'][$section] = array();
  602. }
  603. if (count($settings) == 0) {
  604. throw new Exception("Check your \$options: option $option has no settings!");
  605. }
  606. // set the first setting in the original configuration
  607. $setting = current($settings);
  608. $this->_addSetting($this->_configurations[$c], $setting, $section, $option);
  609. // create clones for each of the other settings
  610. while ($setting = next($settings)) {
  611. $clone = $this->_configurations[$c];
  612. $this->_configurations[] = $this->_addSetting($clone, $setting, $section, $option);
  613. }
  614. reset($settings);
  615. }
  616. return $this->_generateConfigurations();
  617. }
  618. /**
  619. * add a setting to the given configuration
  620. *
  621. * @param array $configuration
  622. * @param array $setting
  623. * @param string $section
  624. * @param string $option
  625. * @throws Exception
  626. * @return array
  627. */
  628. private function _addSetting(&$configuration, &$setting, &$section, &$option) {
  629. if (++$this->_iterationCount > self::MAX_ITERATIONS) {
  630. echo 'max iterations reached, stopping', PHP_EOL;
  631. return $configuration;
  632. }
  633. echo "addSetting: iteration $this->_iterationCount", PHP_EOL;
  634. if (
  635. array_key_exists($option, $configuration['options'][$section]) &&
  636. $configuration['options'][$section][$option] === $setting['setting']
  637. ) {
  638. $val = helper::var_export_min($setting['setting'], true);
  639. throw new Exception("Endless loop or error in options detected: option '$option' already exists with setting '$val' in one of the configurations!");
  640. }
  641. $configuration['options'][$section][$option] = $setting['setting'];
  642. $configuration['tests'][$option] = array($setting['tests'], $setting['affects']);
  643. foreach ($setting['affects'] as $affects) {
  644. if (!in_array($affects, $configuration['affects'])) {
  645. $configuration['affects'][] = $affects;
  646. }
  647. }
  648. return $configuration;
  649. }
  650. }