Completed
Push — 2.2 ( 2c3172...080dba )
by Davert
08:46 queued 03:10
created

Yii2::amLoggedInAs()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 14
rs 9.4285
1
<?php
2
namespace Codeception\Module;
3
4
use Codeception\Configuration;
5
use Codeception\Exception\ModuleConfigException;
6
use Codeception\Exception\ModuleException;
7
use Codeception\Lib\Connector\Yii2 as Yii2Connector;
8
use Codeception\Lib\Framework;
9
use Codeception\Lib\Interfaces\ActiveRecord;
10
use Codeception\Lib\Interfaces\PartedModule;
11
use Codeception\Lib\Notification;
12
use Codeception\TestInterface;
13
use Yii;
14
use yii\db\ActiveRecordInterface;
15
16
/**
17
 * This module provides integration with [Yii framework](http://www.yiiframework.com/) (2.0).
18
 * It initializes Yii framework in test environment and provides actions for functional testing.
19
 *
20
 * ## Config
21
 *
22
 * * `configFile` *required* - the path to the application config file. File should be configured for test environment and return configuration array.
23
 * * `entryUrl` - initial application url (default: http://localhost/index-test.php).
24
 * * `entryScript` - front script title (like: index-test.php). If not set - taken from entryUrl.
25
 * * `cleanup` - (default: true) wrap all database connection inside a transaction and roll it back after the test. Should be disabled for acceptance testing..
26
 *
27
 * You can use this module by setting params in your functional.suite.yml:
28
 *
29
 * ```yaml
30
 * class_name: FunctionalTester
31
 * modules:
32
 *     enabled:
33
 *         - Yii2:
34
 *             configFile: '/path/to/config.php'
35
 * ```
36
 *
37
 * ### Parts
38
 *
39
 * * `init` - use module only for initialization (for acceptance tests).
40
 * * `orm` - include only haveRecord/grabRecord/seeRecord/dontSeeRecord actions
41
 *
42
 * ### Example (`functional.suite.yml`)
43
 *
44
 * ```yml
45
 * class_name: FunctionalTester
46
 * modules:
47
 *   enabled:
48
 *      - Yii2:
49
 *          configFile: 'config/test.php'
50
 * ```
51
 *
52
 * ### Example (`unit.suite.yml`)
53
 *
54
 * ```yml
55
 * class_name: UnitTester
56
 * modules:
57
 *   enabled:
58
 *      - Asserts
59
 *      - Yii2:
60
 *          configFile: 'config/test.php'
61
 *          part: init
62
 * ```
63
 *
64
 * ### Example (`acceptance.suite.yml`)
65
 *
66
 * ```yml
67
 * class_name: AcceptanceTester
68
 * modules:
69
 *     enabled:
70
 *         - WebDriver:
71
 *             url: http://127.0.0.1:8080/
72
 *             browser: firefox
73
 *         - Yii2:
74
 *             configFile: 'config/test.php'
75
 *             part: ORM # allow to use AR methods
76
 *             cleanup: false # don't wrap test in transaction
77
 *             entryScript: index-test.php
78
 * ```
79
 *
80
 * ## Status
81
 *
82
 * Maintainer: **samdark**
83
 * Stability: **stable**
84
 *
85
 */
86
class Yii2 extends Framework implements ActiveRecord, PartedModule
87
{
88
    /**
89
     * Application config file must be set.
90
     * @var array
91
     */
92
    protected $config = [
93
        'cleanup'     => false,
94
        'entryScript' => '',
95
        'entryUrl'    => 'http://localhost/index-test.php',
96
    ];
97
98
    protected $requiredFields = ['configFile'];
99
    protected $transaction;
100
101
    /**
102
     * @var \yii\base\Application
103
     */
104
    public $app;
105
106
    /**
107
     * @var Yii2Connector\FixturesStore[]
108
     */
109
    public $loadedFixtures = [];
110
111
    public function _initialize()
112
    {
113
        if (!is_file(codecept_root_dir() . $this->config['configFile'])) {
114
            throw new ModuleConfigException(
115
                __CLASS__,
116
                "The application config file does not exist: " . codecept_root_dir() . $this->config['configFile']
117
            );
118
        }
119
        $this->defineConstants();
120
    }
121
122
    public function _before(TestInterface $test)
123
    {
124
        $entryUrl = $this->config['entryUrl'];
125
        $entryFile = $this->config['entryScript'] ?: basename($entryUrl);
126
        $entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH);
127
128
        $this->client = new Yii2Connector();
129
        $this->client->defaultServerVars = [
130
            'SCRIPT_FILENAME' => $entryFile,
131
            'SCRIPT_NAME'     => $entryScript,
132
            'SERVER_NAME'     => parse_url($entryUrl, PHP_URL_HOST),
133
            'SERVER_PORT'     => parse_url($entryUrl, PHP_URL_PORT) ?: '80',
134
        ];
135
        $this->client->defaultServerVars['HTTPS'] = parse_url($entryUrl, PHP_URL_SCHEME) === 'https';
136
        $this->client->restoreServerVars();
137
        $this->client->configFile = Configuration::projectDir() . $this->config['configFile'];
138
        $this->app = $this->client->getApplication();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->client->getApplication() can also be of type object<yii\web\Application>. However, the property $app is declared as type object<yii\base\Application>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
139
140
        if ($this->config['cleanup'] && $this->app->has('db')) {
141
            $this->transaction = $this->app->db->beginTransaction();
142
        }
143
    }
144
145
    public function _after(\Codeception\TestInterface $test)
0 ignored issues
show
Coding Style introduced by
_after uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_after uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_after uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_after uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_after uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_after uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
146
    {
147
        $_SESSION = [];
148
        $_FILES = [];
149
        $_GET = [];
150
        $_POST = [];
151
        $_COOKIE = [];
152
        $_REQUEST = [];
153
154
        foreach ($this->loadedFixtures as $fixture) {
155
            $fixture->unloadFixtures();
156
        }
157
158
        if ($this->transaction && $this->config['cleanup']) {
159
            $this->transaction->rollback();
160
        }
161
162
        $this->client->resetPersistentVars();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\BrowserKit\Client as the method resetPersistentVars() does only exist in the following sub-classes of Symfony\Component\BrowserKit\Client: Codeception\Lib\Connector\Yii2. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
163
164
        if (\Yii::$app->has('session', true)) {
165
            \Yii::$app->session->close();
166
        }
167
        parent::_after($test);
168
    }
169
170
    public function _parts()
171
    {
172
        return ['orm', 'init', 'fixtures', 'email'];
173
    }
174
175
    /**
176
     * Authorizes user on a site without submitting login form.
177
     * Use it for fast pragmatic authorization in functional tests.
178
     *
179
     * ```php
180
     * <?php
181
     * // User is found by id
182
     * $I->amLoggedInAs(1);
183
     *
184
     * // User object is passed as parameter
185
     * $admin = \app\models\User::findByUsername('admin');
186
     * $I->amLoggedInAs($admin);
187
     * ```
188
     * Requires `user` component to be enabled and configured.
189
     *
190
     * @param $user
191
     * @throws ModuleException
192
     */
193
    public function amLoggedInAs($user)
194
    {
195
        if (!Yii::$app->has('user')) {
196
            throw new ModuleException($this, 'User component is not loaded');
197
        }
198
        if ($user instanceof \yii\web\IdentityInterface) {
0 ignored issues
show
Bug introduced by
The class yii\web\IdentityInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
199
            $identity = $user;
200
        } else {
201
            // class name implementing IdentityInterface
202
            $identityClass = Yii::$app->user->identityClass;
203
            $identity = call_user_func([$identityClass, 'findIdentity'], $user);
204
        }
205
        Yii::$app->user->login($identity);
206
    }
207
208
    /**
209
     * Creates and loads fixtures from a config.
210
     * Signature is the same as for `fixtures()` method of `yii\test\FixtureTrait`
211
     *
212
     * ```php
213
     * <?php
214
     * $I->haveFixtures(,
215
     *     'posts' => PostsFixture::className(),
216
     *     'user' => [
217
     *         'class' => UserFixture::className(),
218
     *         'dataFile' => '@tests/_data/models/user.php'
219
     *      ],
220
     * );
221
     * ```
222
     *
223
     * @param $fixtures
224
     * @part fixtures
225
     */
226
    public function haveFixtures($fixtures)
227
    {
228
        $fixturesStore = new Yii2Connector\FixturesStore($fixtures);
229
        $fixturesStore->loadFixtures();
230
        $this->loadedFixtures[] = $fixturesStore;
231
    }
232
233
    /**
234
     * Returns all loaded fixtures.
235
     * Array of fixture instances
236
     *
237
     * @part fixtures
238
     * @return array
239
     */
240
    public function grabFixtures()
241
    {
242
        return call_user_func_array('array_merge',
243
            array_map( // merge all fixtures from all fixture stores
244
                function ($fixturesStore) {
245
                    return $fixturesStore->getFixtures();
246
                },
247
                $this->loadedFixtures
248
            )
249
        );
250
    }
251
252
    /**
253
     * Gets a fixture by name.
254
     * Returns a Fixture instance. If a fixture is an instance of `\yii\test\BaseActiveFixture` a second parameter
255
     * can be used to return a specific model:
256
     *
257
     * ```php
258
     * <?php
259
     * $I->haveFixtures(['users' => UserFixture::className()]);
260
     *
261
     * $users = $I->grabFixture('users');
262
     *
263
     * // get first user by key, if a fixture is instance of ActiveFixture
264
     * $user = $I->grabFixture('users', 'user1');
265
     * ```
266
     *
267
     * @param $name
268
     * @return mixed
269
     * @throws ModuleException if a fixture is not found
270
     * @part fixtures
271
     */
272
    public function grabFixture($name, $index = null)
273
    {
274
        $fixtures = $this->grabFixtures();
275
        if (!isset($fixtures[$name])) {
276
            throw new ModuleException($this, "Fixture $name is not loaded");
277
        }
278
        $fixture = $fixtures[$name];
279
        if ($index === null) {
280
            return $fixture;
281
        }
282
        if ($fixture instanceof \yii\test\BaseActiveFixture) {
0 ignored issues
show
Bug introduced by
The class yii\test\BaseActiveFixture does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
283
            return $fixture->getModel($index);
284
        }
285
        throw new ModuleException($this, "Fixture $name is not an instance of ActiveFixture and can't be loaded with scond parameter");
286
    }
287
288
    /**
289
     * Inserts record into the database.
290
     *
291
     * ``` php
292
     * <?php
293
     * $user_id = $I->haveRecord('app\models\User', array('name' => 'Davert'));
294
     * ?>
295
     * ```
296
     *
297
     * @param $model
298
     * @param array $attributes
299
     * @return mixed
300
     * @part orm
301
     */
302
    public function haveRecord($model, $attributes = [])
303
    {
304
        /** @var $record \yii\db\ActiveRecord  * */
305
        $record = $this->getModelRecord($model);
306
        $record->setAttributes($attributes, false);
307
        $res = $record->save(false);
308
        if (!$res) {
309
            $this->fail("Record $model was not saved");
310
        }
311
        return $record->primaryKey;
312
    }
313
314
    /**
315
     * Checks that record exists in database.
316
     *
317
     * ``` php
318
     * $I->seeRecord('app\models\User', array('name' => 'davert'));
319
     * ```
320
     *
321
     * @param $model
322
     * @param array $attributes
323
     * @part orm
324
     */
325 View Code Duplication
    public function seeRecord($model, $attributes = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
326
    {
327
        $record = $this->findRecord($model, $attributes);
328
        if (!$record) {
329
            $this->fail("Couldn't find $model with " . json_encode($attributes));
330
        }
331
        $this->debugSection($model, json_encode($record));
332
    }
333
334
    /**
335
     * Checks that record does not exist in database.
336
     *
337
     * ``` php
338
     * $I->dontSeeRecord('app\models\User', array('name' => 'davert'));
339
     * ```
340
     *
341
     * @param $model
342
     * @param array $attributes
343
     * @part orm
344
     */
345 View Code Duplication
    public function dontSeeRecord($model, $attributes = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
346
    {
347
        $record = $this->findRecord($model, $attributes);
348
        $this->debugSection($model, json_encode($record));
349
        if ($record) {
350
            $this->fail("Unexpectedly managed to find $model with " . json_encode($attributes));
351
        }
352
    }
353
354
    /**
355
     * Retrieves record from database
356
     *
357
     * ``` php
358
     * $category = $I->grabRecord('app\models\User', array('name' => 'davert'));
359
     * ```
360
     *
361
     * @param $model
362
     * @param array $attributes
363
     * @return mixed
364
     * @part orm
365
     */
366
    public function grabRecord($model, $attributes = [])
367
    {
368
        return $this->findRecord($model, $attributes);
369
    }
370
371
    protected function findRecord($model, $attributes = [])
372
    {
373
        $this->getModelRecord($model);
374
        return call_user_func([$model, 'find'])
375
            ->where($attributes)
376
            ->one();
377
    }
378
379 View Code Duplication
    protected function getModelRecord($model)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
380
    {
381
        if (!class_exists($model)) {
382
            throw new \RuntimeException("Model $model does not exist");
383
        }
384
        $record = new $model;
385
        if (!$record instanceof ActiveRecordInterface) {
0 ignored issues
show
Bug introduced by
The class yii\db\ActiveRecordInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
386
            throw new \RuntimeException("Model $model is not implement interface \\yii\\db\\ActiveRecordInterface");
387
        }
388
        return $record;
389
    }
390
391
    /**
392
     * Converting $page to valid Yii 2 URL
393
     *
394
     * Allows input like:
395
     *
396
     * ```php
397
     * <?php
398
     * $I->amOnPage(['site/view','page'=>'about']);
399
     * $I->amOnPage('index-test.php?site/index');
400
     * $I->amOnPage('http://localhost/index-test.php?site/index');
401
     * ```
402
     *
403
     * @param $page string|array parameter for \yii\web\UrlManager::createUrl()
404
     */
405
    public function amOnPage($page)
406
    {
407
        if (is_array($page)) {
408
            $page = Yii::$app->getUrlManager()->createUrl($page);
409
        }
410
        parent::amOnPage($page);
411
    }
412
413
    /**
414
     * Similar to amOnPage but accepts route as first argument and params as second
415
     *
416
     * ```
417
     * $I->amOnRoute('site/view', ['page' => 'about']);
418
     * ```
419
     *
420
     */
421
    public function amOnRoute($route, array $params = [])
422
    {
423
        array_unshift($params, $route);
424
        $this->amOnPage($params);
425
    }
426
427
    /**
428
     * Gets a component from Yii container. Throws exception if component is not available
429
     *
430
     * ```php
431
     * <?php
432
     * $mailer = $I->grabComponent('mailer');
433
     * ```
434
     *
435
     * @param $component
436
     * @return mixed
437
     * @throws ModuleException
438
     */
439
    public function grabComponent($component)
440
    {
441
        if (!Yii::$app->has($component)) {
442
            throw new ModuleException($this, "Component $component is not avilable in current application");
443
        }
444
        return Yii::$app->get($component);
445
    }
446
447
    /**
448
     * Checks that email is sent.
449
     *
450
     * ```php
451
     * <?php
452
     * // check that at least 1 email was sent
453
     * $I->seeEmailIsSent();
454
     *
455
     * // check that only 3 emails were sent
456
     * $I->seeEmailIsSent(3);
457
     * ```
458
     *
459
     * @param int $num
460
     * @throws ModuleException
461
     * @part email
462
     */
463
    public function seeEmailIsSent($num = null)
464
    {
465
        if ($num === null) {
466
            $this->assertNotEmpty($this->grabSentEmails(), 'emails were sent');
467
            return;
468
        }
469
        $this->assertEquals($num, count($this->grabSentEmails()), 'number of sent emails is equal to ' . $num);
470
    }
471
472
    /**
473
     * Checks that no email was sent
474
     *
475
     * @part email
476
     */
477
    public function dontSeeEmailIsSent()
478
    {
479
        $this->seeEmailIsSent(0);
480
    }
481
482
    /**
483
     * Returns array of all sent email messages.
484
     * Each message implements `yii\mail\Message` interface.
485
     * Useful to perform additional checks using `Asserts` module:
486
     *
487
     * ```php
488
     * <?php
489
     * $I->seeEmailIsSent();
490
     * $messages = $I->grabSentEmails();
491
     * $I->assertEquals('admin@site,com', $messages[0]->getTo());
492
     * ```
493
     *
494
     * @part email
495
     * @return array
496
     * @throws ModuleException
497
     */
498
    public function grabSentEmails()
499
    {
500
        $mailer = $this->grabComponent('mailer');
501
        if (!$mailer instanceof Yii2Connector\TestMailer) {
502
            throw new ModuleException($this, "Mailer module is not mocked, can't test emails");
503
        }
504
        return $mailer->getSentMessages();
505
    }
506
507
    /**
508
     * Returns last sent email:
509
     *
510
     * ```php
511
     * <?php
512
     * $I->seeEmailIsSent();
513
     * $message = $I->grabLastSentEmail();
514
     * $I->assertEquals('admin@site,com', $message->getTo());
515
     * ```
516
     * @part email
517
     */
518
    public function grabLastSentEmail()
519
    {
520
        $this->seeEmailIsSent();
521
        $messages = $this->grabSentEmails();
522
        return end($messages);
523
    }
524
525
    /**
526
     * Getting domain regex from rule host template
527
     *
528
     * @param string $template
529
     * @return string
530
     */
531
    private function getDomainRegex($template)
532
    {
533
        if (preg_match('#https?://(.*)#', $template, $matches)) {
534
            $template = $matches[1];
535
        }
536
        $parameters = [];
537
        if (strpos($template, '<') !== false) {
538
            $template = preg_replace_callback(
539
                '/<(?:\w+):?([^>]+)?>/u',
540
                function ($matches) use (&$parameters) {
541
                    $key = '#' . count($parameters) . '#';
542
                    $parameters[$key] = isset($matches[1]) ? $matches[1] : '\w+';
543
                    return $key;
544
                },
545
                $template
546
            );
547
        }
548
        $template = preg_quote($template);
549
        $template = strtr($template, $parameters);
550
        return '/^' . $template . '$/u';
551
    }
552
553
    /**
554
     * Returns a list of regex patterns for recognized domain names
555
     *
556
     * @return array
557
     */
558
    public function getInternalDomains()
559
    {
560
        $domains = [$this->getDomainRegex(Yii::$app->urlManager->hostInfo)];
561
562
        if (Yii::$app->urlManager->enablePrettyUrl) {
563
            foreach (Yii::$app->urlManager->rules as $rule) {
564
                /** @var \yii\web\UrlRule $rule */
565
                if (isset($rule->host)) {
566
                    $domains[] = $this->getDomainRegex($rule->host);
567
                }
568
            }
569
        }
570
        return array_unique($domains);
571
    }
572
573
    private function defineConstants()
574
    {
575
        defined('YII_DEBUG') or define('YII_DEBUG', true);
576
        defined('YII_ENV') or define('YII_ENV', 'test');
577
        defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', false);
578
579
        if (YII_ENV !== 'test') {
580
            Notification::warning("YII_ENV is not set to `test`, please add \n\n`define(\'YII_ENV\', \'test\');`\n\nto bootstrap file", 'Yii Framework');
581
        }
582
    }
583
}
584