Lumen.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. <?php
  2. namespace Codeception\Module;
  3. use Codeception\Configuration;
  4. use Codeception\Exception\ModuleException;
  5. use Codeception\Exception\ModuleConfigException;
  6. use Codeception\Lib\Connector\Lumen as LumenConnector;
  7. use Codeception\Lib\Framework;
  8. use Codeception\Lib\Interfaces\ActiveRecord;
  9. use Codeception\Lib\Interfaces\PartedModule;
  10. use Codeception\Lib\Shared\LaravelCommon;
  11. use Codeception\Lib\ModuleContainer;
  12. use Codeception\TestInterface;
  13. use Codeception\Util\ReflectionHelper;
  14. use Illuminate\Contracts\Auth\Authenticatable;
  15. use Illuminate\Database\Eloquent\Model as EloquentModel;
  16. /**
  17. *
  18. * This module allows you to run functional tests for Lumen.
  19. * Please try it and leave your feedback.
  20. *
  21. * ## Demo project
  22. * <https://github.com/janhenkgerritsen/codeception-lumen-sample>
  23. *
  24. * ## Status
  25. *
  26. * * Maintainer: **Jan-Henk Gerritsen**
  27. * * Stability: **dev**
  28. * * Contact: janhenkgerritsen@gmail.com
  29. *
  30. * ## Config
  31. *
  32. * * cleanup: `boolean`, default `true` - all database queries will be run in a transaction,
  33. * which will be rolled back at the end of each test.
  34. * * bootstrap: `string`, default `bootstrap/app.php` - relative path to app.php config file.
  35. * * root: `string`, default `` - root path of the application.
  36. * * packages: `string`, default `workbench` - root path of application packages (if any).
  37. * * url: `string`, default `http://localhost` - the application URL
  38. *
  39. * ## API
  40. *
  41. * * app - `\Laravel\Lumen\Application`
  42. * * config - `array`
  43. *
  44. * ## Parts
  45. *
  46. * * ORM - only include the database methods of this module:
  47. * * have
  48. * * haveMultiple
  49. * * haveRecord
  50. * * grabRecord
  51. * * seeRecord
  52. * * dontSeeRecord
  53. */
  54. class Lumen extends Framework implements ActiveRecord, PartedModule
  55. {
  56. use LaravelCommon;
  57. /**
  58. * @var \Laravel\Lumen\Application
  59. */
  60. public $app;
  61. /**
  62. * @var array
  63. */
  64. public $config = [];
  65. /**
  66. * Constructor.
  67. *
  68. * @param ModuleContainer $container
  69. * @param array|null $config
  70. */
  71. public function __construct(ModuleContainer $container, $config = null)
  72. {
  73. $this->config = array_merge(
  74. [
  75. 'cleanup' => true,
  76. 'bootstrap' => 'bootstrap' . DIRECTORY_SEPARATOR . 'app.php',
  77. 'root' => '',
  78. 'packages' => 'workbench',
  79. 'url' => 'http://localhost',
  80. ],
  81. (array)$config
  82. );
  83. $projectDir = explode($this->config['packages'], Configuration::projectDir())[0];
  84. $projectDir .= $this->config['root'];
  85. $this->config['project_dir'] = $projectDir;
  86. $this->config['bootstrap_file'] = $projectDir . $this->config['bootstrap'];
  87. parent::__construct($container);
  88. }
  89. /**
  90. * @return array
  91. */
  92. public function _parts()
  93. {
  94. return ['orm'];
  95. }
  96. /**
  97. * Initialize hook.
  98. */
  99. public function _initialize()
  100. {
  101. $this->checkBootstrapFileExists();
  102. $this->registerAutoloaders();
  103. }
  104. /**
  105. * Before hook.
  106. *
  107. * @param \Codeception\TestInterface $test
  108. * @throws ModuleConfigException
  109. */
  110. public function _before(TestInterface $test)
  111. {
  112. $this->client = new LumenConnector($this);
  113. if ($this->app['db'] && $this->config['cleanup']) {
  114. $this->app['db']->beginTransaction();
  115. }
  116. }
  117. /**
  118. * After hook.
  119. *
  120. * @param \Codeception\TestInterface $test
  121. */
  122. public function _after(TestInterface $test)
  123. {
  124. if ($this->app['db'] && $this->config['cleanup']) {
  125. $this->app['db']->rollback();
  126. }
  127. // disconnect from DB to prevent "Too many connections" issue
  128. if ($this->app['db']) {
  129. $this->app['db']->disconnect();
  130. }
  131. }
  132. /**
  133. * Make sure the Lumen bootstrap file exists.
  134. *
  135. * @throws ModuleConfigException
  136. */
  137. protected function checkBootstrapFileExists()
  138. {
  139. $bootstrapFile = $this->config['bootstrap_file'];
  140. if (!file_exists($bootstrapFile)) {
  141. throw new ModuleConfigException(
  142. $this,
  143. "Lumen bootstrap file not found in $bootstrapFile.\n"
  144. . "Please provide a valid path using the 'bootstrap' config param. "
  145. );
  146. }
  147. }
  148. /**
  149. * Register autoloaders.
  150. */
  151. protected function registerAutoloaders()
  152. {
  153. require $this->config['project_dir'] . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
  154. }
  155. /**
  156. * Provides access the Lumen application object.
  157. *
  158. * @return \Laravel\Lumen\Application
  159. */
  160. public function getApplication()
  161. {
  162. return $this->app;
  163. }
  164. /**
  165. * @param \Laravel\Lumen\Application $app
  166. */
  167. public function setApplication($app)
  168. {
  169. $this->app = $app;
  170. }
  171. /**
  172. * Opens web page using route name and parameters.
  173. *
  174. * ```php
  175. * <?php
  176. * $I->amOnRoute('homepage');
  177. * ?>
  178. * ```
  179. *
  180. * @param $routeName
  181. * @param array $params
  182. */
  183. public function amOnRoute($routeName, $params = [])
  184. {
  185. $route = $this->getRouteByName($routeName);
  186. if (!$route) {
  187. $this->fail("Could not find route with name '$routeName'");
  188. }
  189. $url = $this->generateUrlForRoute($route, $params);
  190. $this->amOnPage($url);
  191. }
  192. /**
  193. * Get the route for a route name.
  194. *
  195. * @param string $routeName
  196. * @return array|null
  197. */
  198. private function getRouteByName($routeName)
  199. {
  200. if (isset($this->app->router) && $this->app->router instanceof \Laravel\Lumen\Routing\Router) {
  201. $router = $this->app->router;
  202. } else {
  203. // backward compatibility with lumen 5.3
  204. $router = $this->app;
  205. }
  206. foreach ($router->getRoutes() as $route) {
  207. if (isset($route['action']['as']) && $route['action']['as'] == $routeName) {
  208. return $route;
  209. }
  210. }
  211. $this->fail("Route with name '$routeName' does not exist");
  212. return null;
  213. }
  214. /**
  215. * Generate the URL for a route specification.
  216. * Replaces the route parameters from left to right with the parameters
  217. * passed in the $params array.
  218. *
  219. * @param array $route
  220. * @param array $params
  221. * @return string
  222. */
  223. private function generateUrlForRoute($route, $params)
  224. {
  225. $url = $route['uri'];
  226. while (count($params) > 0) {
  227. $param = array_shift($params);
  228. $url = preg_replace('/{.+?}/', $param, $url, 1);
  229. }
  230. return $url;
  231. }
  232. /**
  233. * Set the authenticated user for the next request.
  234. * This will not persist between multiple requests.
  235. *
  236. * @param \Illuminate\Contracts\Auth\Authenticatable
  237. * @param string|null $driver The authentication driver for Lumen <= 5.1.*, guard name for Lumen >= 5.2
  238. * @return void
  239. */
  240. public function amLoggedAs($user, $driver = null)
  241. {
  242. if (!$user instanceof Authenticatable) {
  243. $this->fail(
  244. 'The user passed to amLoggedAs() should be an instance of \\Illuminate\\Contracts\\Auth\\Authenticatable'
  245. );
  246. }
  247. $guard = $auth = $this->app['auth'];
  248. if (method_exists($auth, 'driver')) {
  249. $guard = $auth->driver($driver);
  250. }
  251. if (method_exists($auth, 'guard')) {
  252. $guard = $auth->guard($driver);
  253. }
  254. $guard->setUser($user);
  255. }
  256. /**
  257. * Checks that user is authenticated.
  258. */
  259. public function seeAuthentication()
  260. {
  261. $this->assertTrue($this->app['auth']->check(), 'User is not logged in');
  262. }
  263. /**
  264. * Check that user is not authenticated.
  265. */
  266. public function dontSeeAuthentication()
  267. {
  268. $this->assertFalse($this->app['auth']->check(), 'User is logged in');
  269. }
  270. /**
  271. * Return an instance of a class from the IoC Container.
  272. *
  273. * Example
  274. * ``` php
  275. * <?php
  276. * // In Lumen
  277. * App::bind('foo', function($app)
  278. * {
  279. * return new FooBar;
  280. * });
  281. *
  282. * // Then in test
  283. * $service = $I->grabService('foo');
  284. *
  285. * // Will return an instance of FooBar, also works for singletons.
  286. * ?>
  287. * ```
  288. *
  289. * @param string $class
  290. * @return mixed
  291. */
  292. public function grabService($class)
  293. {
  294. return $this->app[$class];
  295. }
  296. /**
  297. * Inserts record into the database.
  298. * If you pass the name of a database table as the first argument, this method returns an integer ID.
  299. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model.
  300. *
  301. * ``` php
  302. * <?php
  303. * $user_id = $I->haveRecord('users', array('name' => 'Davert')); // returns integer
  304. * $user = $I->haveRecord('App\User', array('name' => 'Davert')); // returns Eloquent model
  305. * ?>
  306. * ```
  307. *
  308. * @param string $table
  309. * @param array $attributes
  310. * @return integer|EloquentModel
  311. * @part orm
  312. */
  313. public function haveRecord($table, $attributes = [])
  314. {
  315. if (class_exists($table)) {
  316. $model = new $table;
  317. if (!$model instanceof EloquentModel) {
  318. throw new \RuntimeException("Class $table is not an Eloquent model");
  319. }
  320. $model->fill($attributes)->save();
  321. return $model;
  322. }
  323. try {
  324. return $this->app['db']->table($table)->insertGetId($attributes);
  325. } catch (\Exception $e) {
  326. $this->fail("Could not insert record into table '$table':\n\n" . $e->getMessage());
  327. }
  328. }
  329. /**
  330. * Checks that record exists in database.
  331. * You can pass the name of a database table or the class name of an Eloquent model as the first argument.
  332. *
  333. * ``` php
  334. * <?php
  335. * $I->seeRecord('users', array('name' => 'davert'));
  336. * $I->seeRecord('App\User', array('name' => 'davert'));
  337. * ?>
  338. * ```
  339. *
  340. * @param string $table
  341. * @param array $attributes
  342. * @part orm
  343. */
  344. public function seeRecord($table, $attributes = [])
  345. {
  346. if (class_exists($table)) {
  347. if (!$this->findModel($table, $attributes)) {
  348. $this->fail("Could not find $table with " . json_encode($attributes));
  349. }
  350. } elseif (!$this->findRecord($table, $attributes)) {
  351. $this->fail("Could not find matching record in table '$table'");
  352. }
  353. }
  354. /**
  355. * Checks that record does not exist in database.
  356. * You can pass the name of a database table or the class name of an Eloquent model as the first argument.
  357. *
  358. * ``` php
  359. * <?php
  360. * $I->dontSeeRecord('users', array('name' => 'davert'));
  361. * $I->dontSeeRecord('App\User', array('name' => 'davert'));
  362. * ?>
  363. * ```
  364. *
  365. * @param string $table
  366. * @param array $attributes
  367. * @part orm
  368. */
  369. public function dontSeeRecord($table, $attributes = [])
  370. {
  371. if (class_exists($table)) {
  372. if ($this->findModel($table, $attributes)) {
  373. $this->fail("Unexpectedly found matching $table with " . json_encode($attributes));
  374. }
  375. } elseif ($this->findRecord($table, $attributes)) {
  376. $this->fail("Unexpectedly found matching record in table '$table'");
  377. }
  378. }
  379. /**
  380. * Retrieves record from database
  381. * If you pass the name of a database table as the first argument, this method returns an array.
  382. * You can also pass the class name of an Eloquent model, in that case this method returns an Eloquent model.
  383. *
  384. * ``` php
  385. * <?php
  386. * $record = $I->grabRecord('users', array('name' => 'davert')); // returns array
  387. * $record = $I->grabRecord('App\User', array('name' => 'davert')); // returns Eloquent model
  388. * ?>
  389. * ```
  390. *
  391. * @param string $table
  392. * @param array $attributes
  393. * @return array|EloquentModel
  394. * @part orm
  395. */
  396. public function grabRecord($table, $attributes = [])
  397. {
  398. if (class_exists($table)) {
  399. if (!$model = $this->findModel($table, $attributes)) {
  400. $this->fail("Could not find $table with " . json_encode($attributes));
  401. }
  402. return $model;
  403. }
  404. if (!$record = $this->findRecord($table, $attributes)) {
  405. $this->fail("Could not find matching record in table '$table'");
  406. }
  407. return $record;
  408. }
  409. /**
  410. * @param string $modelClass
  411. * @param array $attributes
  412. *
  413. * @return EloquentModel
  414. */
  415. protected function findModel($modelClass, $attributes = [])
  416. {
  417. $model = new $modelClass;
  418. if (!$model instanceof EloquentModel) {
  419. throw new \RuntimeException("Class $modelClass is not an Eloquent model");
  420. }
  421. $query = $model->newQuery();
  422. foreach ($attributes as $key => $value) {
  423. $query->where($key, $value);
  424. }
  425. return $query->first();
  426. }
  427. /**
  428. * @param string $table
  429. * @param array $attributes
  430. * @return array
  431. */
  432. protected function findRecord($table, $attributes = [])
  433. {
  434. $query = $this->app['db']->table($table);
  435. foreach ($attributes as $key => $value) {
  436. $query->where($key, $value);
  437. }
  438. return (array)$query->first();
  439. }
  440. /**
  441. * Use Lumen's model factory to create a model.
  442. * Can only be used with Lumen 5.1 and later.
  443. *
  444. * ``` php
  445. * <?php
  446. * $I->have('App\User');
  447. * $I->have('App\User', ['name' => 'John Doe']);
  448. * $I->have('App\User', [], 'admin');
  449. * ?>
  450. * ```
  451. *
  452. * @see https://lumen.laravel.com/docs/master/testing#model-factories
  453. * @param string $model
  454. * @param array $attributes
  455. * @param string $name
  456. * @return mixed
  457. * @part orm
  458. */
  459. public function have($model, $attributes = [], $name = 'default')
  460. {
  461. try {
  462. return $this->modelFactory($model, $name)->create($attributes);
  463. } catch (\Exception $e) {
  464. $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage());
  465. }
  466. }
  467. /**
  468. * Use Laravel's model factory to create multiple models.
  469. * Can only be used with Lumen 5.1 and later.
  470. *
  471. * ``` php
  472. * <?php
  473. * $I->haveMultiple('App\User', 10);
  474. * $I->haveMultiple('App\User', 10, ['name' => 'John Doe']);
  475. * $I->haveMultiple('App\User', 10, [], 'admin');
  476. * ?>
  477. * ```
  478. *
  479. * @see https://lumen.laravel.com/docs/master/testing#model-factories
  480. * @param string $model
  481. * @param int $times
  482. * @param array $attributes
  483. * @param string $name
  484. * @return mixed
  485. * @part orm
  486. */
  487. public function haveMultiple($model, $times, $attributes = [], $name = 'default')
  488. {
  489. try {
  490. return $this->modelFactory($model, $name, $times)->create($attributes);
  491. } catch (\Exception $e) {
  492. $this->fail("Could not create model: \n\n" . get_class($e) . "\n\n" . $e->getMessage());
  493. }
  494. }
  495. /**
  496. * @param string $model
  497. * @param string $name
  498. * @param int $times
  499. * @return \Illuminate\Database\Eloquent\FactoryBuilder
  500. * @throws ModuleException
  501. */
  502. protected function modelFactory($model, $name, $times = 1)
  503. {
  504. if (!function_exists('factory')) {
  505. throw new ModuleException($this, 'The factory() method does not exist. ' .
  506. 'This functionality relies on Lumen model factories, which were introduced in Lumen 5.1.');
  507. }
  508. return factory($model, $name, $times);
  509. }
  510. /**
  511. * Returns a list of recognized domain names.
  512. * This elements of this list are regular expressions.
  513. *
  514. * @return array
  515. */
  516. protected function getInternalDomains()
  517. {
  518. $server = ReflectionHelper::readPrivateProperty($this->client, 'server');
  519. return ['/^' . str_replace('.', '\.', $server['HTTP_HOST']) . '$/'];
  520. }
  521. }