1
0

configuration-test-generator 24 KB

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