diff --git a/api/config/config.php b/api/config/config.php index ae816f3..ff3123d 100644 --- a/api/config/config.php +++ b/api/config/config.php @@ -70,10 +70,13 @@ return [ 'modules' => [ 'authserver' => [ 'class' => api\modules\authserver\Module::class, - 'baseDomain' => getenv('AUTHSERVER_HOST'), + 'host' => $params['authserverHost'], ], 'session' => [ 'class' => api\modules\session\Module::class, ], + 'mojang' => [ + 'class' => api\modules\mojang\Module::class, + ], ], ]; diff --git a/api/config/routes.php b/api/config/routes.php index 6dec26d..7ff19d2 100644 --- a/api/config/routes.php +++ b/api/config/routes.php @@ -1,4 +1,7 @@ 'accounts/change-email-initialize', '/accounts/change-email/submit-new-email' => 'accounts/change-email-submit-new-email', @@ -13,4 +16,12 @@ return [ '/minecraft/session/hasJoined' => 'session/session/has-joined', '/minecraft/session/legacy/hasJoined' => 'session/session/has-joined-legacy', '/minecraft/session/profile/' => 'session/session/profile', + + '/mojang/profiles/' => 'mojang/api/uuid-by-username', + '/mojang/profiles//names' => 'mojang/api/usernames-by-uuid', + 'POST /mojang/profiles' => 'mojang/api/uuids-by-usernames', + + "http://{$params['authserverHost']}/mojang/api/users/profiles/minecraft/" => 'mojang/api/uuid-by-username', + "http://{$params['authserverHost']}/mojang/api/user/profiles//names" => 'mojang/api/usernames-by-uuid', + "POST http://{$params['authserverHost']}/mojang/api/profiles/minecraft" => 'mojang/api/uuids-by-usernames', ]; diff --git a/api/models/authentication/RegistrationForm.php b/api/models/authentication/RegistrationForm.php index 8122c4d..1dfc3ea 100644 --- a/api/models/authentication/RegistrationForm.php +++ b/api/models/authentication/RegistrationForm.php @@ -9,6 +9,7 @@ use common\components\UserFriendlyRandomKey; use common\models\Account; use common\models\confirmations\RegistrationConfirmation; use common\models\EmailActivation; +use common\models\UsernameHistory; use common\validators\LanguageValidator; use common\validators\PasswordValidate; use Ely\Email\Renderer; @@ -108,6 +109,14 @@ class RegistrationForm extends ApiForm { throw new ErrorException('Unable save email-activation model.'); } + $usernamesHistory = new UsernameHistory(); + $usernamesHistory->account_id = $account->id; + $usernamesHistory->username = $account->username; + $usernamesHistory->applied_in = $account->created_at; + if (!$usernamesHistory->save()) { + throw new ErrorException('Cannot save username history record'); + } + $this->sendMail($emailActivation, $account); $changeUsernameForm = new ChangeUsernameForm(); diff --git a/api/modules/authserver/Module.php b/api/modules/authserver/Module.php index d8bcc11..1a4a5e2 100644 --- a/api/modules/authserver/Module.php +++ b/api/modules/authserver/Module.php @@ -15,11 +15,11 @@ class Module extends \yii\base\Module implements BootstrapInterface { /** * @var string базовый домен, запросы на который этот модуль должен обрабатывать */ - public $baseDomain = 'https://authserver.ely.by'; + public $host = 'authserver.ely.by'; public function init() { parent::init(); - if ($this->baseDomain === null) { + if ($this->host === null) { throw new InvalidConfigException('base domain must be specified'); } } @@ -39,7 +39,7 @@ class Module extends \yii\base\Module implements BootstrapInterface { */ public function bootstrap($app) { $app->getUrlManager()->addRules([ - $this->baseDomain . '/' . $this->id . '/auth/' => $this->id . '/authentication/', + "http://$this->host/$this->id/auth/" => "$this->id/authentication/", ], false); } @@ -59,7 +59,7 @@ class Module extends \yii\base\Module implements BootstrapInterface { * @throws NotFoundHttpException */ protected function checkHost() { - if (Yii::$app->request->getHostInfo() !== $this->baseDomain) { + if (parse_url(Yii::$app->request->getHostInfo(), PHP_URL_HOST) !== $this->host) { throw new NotFoundHttpException(); } } diff --git a/api/modules/mojang/Module.php b/api/modules/mojang/Module.php new file mode 100644 index 0000000..9306443 --- /dev/null +++ b/api/modules/mojang/Module.php @@ -0,0 +1,10 @@ +andWhere(['username' => $username]) + ->orderBy(['applied_in' => SORT_DESC]) + ->andWhere(['<=', 'applied_in', $at]) + ->one(); + + // Запрос выше находит просто последний случай использования, не учитывая то, что ник + // мог быть сменён с тех пор. Поэтому дополнительно проводим проверку, чтобы ник находился + // в каком-либо периоде (т.е. существовала последующая запись) или последний использовавший + // ник пользователь не сменил его на нечто иное + $account = null; + if ($record !== null) { + if ($record->findNext($at) !== null || $record->account->username === $record->username) { + $account = $record->account; + } + } + } else { + /** @var Account|null $record */ + $account = Account::findOne(['username' => $username]); + } + + if ($account === null) { + return $this->noContentResponse(); + } + + return [ + 'id' => str_replace('-', '', $account->uuid), + 'name' => $account->username, + ]; + } + + public function actionUsernamesByUuid($uuid) { + try { + $uuid = Uuid::fromString($uuid)->toString(); + } catch(\InvalidArgumentException $e) { + return $this->illegalArgumentResponse('Invalid uuid format.'); + } + + $account = Account::findOne(['uuid' => $uuid]); + if ($account === null) { + return $this->noContentResponse(); + } + + /** @var UsernameHistory[] $usernameHistory */ + $usernameHistory = $account->getUsernameHistory() + ->orderBy(['applied_in' => SORT_ASC]) + ->all(); + + $data = []; + foreach($usernameHistory as $record) { + $data[] = [ + 'name' => $record->username, + 'changedToAt' => $record->applied_in * 1000, + ]; + } + + // У первого элемента не должно быть времени, когда он был применён + // Хотя мы в принципе эту инфу знаем. А вот Mojang, вероятно, нет + unset($data[0]['changedToAt']); + + return $data; + } + + public function actionUuidsByUsernames() { + $usernames = Yii::$app->request->post(); + if (empty($usernames)) { + $usernames = json_decode(Yii::$app->request->getRawBody()); + if (empty($usernames)) { + return $this->illegalArgumentResponse('Passed array of profile names is an invalid JSON string.'); + } + } + + $usernames = array_unique($usernames); + if (count($usernames) > 100) { + return $this->illegalArgumentResponse('Not more that 100 profile name per call is allowed.'); + } + + foreach($usernames as $username) { + if (empty($username) || is_array($username)) { + return $this->illegalArgumentResponse('profileName can not be null, empty or array key.'); + } + } + + /** @var Account[] $accounts */ + $accounts = Account::find() + ->andWhere(['username' => $usernames]) + ->orderBy(['username' => $usernames]) + ->limit(count($usernames)) + ->all(); + + $responseData = []; + foreach($accounts as $account) { + $responseData[] = [ + 'id' => str_replace('-', '', $account->uuid), + 'name' => $account->username, + ]; + } + + return $responseData; + } + + private function noContentResponse() { + $response = Yii::$app->getResponse(); + $response->setStatusCode(204); + $response->format = Response::FORMAT_RAW; + $response->content = ''; + + return $response; + } + + private function illegalArgumentResponse(string $errorMessage) { + $response = Yii::$app->getResponse(); + $response->setStatusCode(400); + $response->format = Response::FORMAT_JSON; + $response->data = [ + 'error' => 'IllegalArgumentException', + 'errorMessage' => $errorMessage, + ]; + + return $response; + } + +} diff --git a/common/config/config.php b/common/config/config.php index 3df1fbb..92f07f9 100644 --- a/common/config/config.php +++ b/common/config/config.php @@ -12,6 +12,9 @@ return [ 'username' => getenv('MYSQL_USER'), 'password' => getenv('MYSQL_PASSWORD'), 'charset' => 'utf8', + 'schemaMap' => [ + 'mysql' => common\db\mysql\Schema::class, + ], ], 'mailer' => [ 'class' => yii\swiftmailer\Mailer::class, diff --git a/common/db/mysql/QueryBuilder.php b/common/db/mysql/QueryBuilder.php new file mode 100644 index 0000000..f467396 --- /dev/null +++ b/common/db/mysql/QueryBuilder.php @@ -0,0 +1,39 @@ + $direction) { + if ($direction instanceof Expression) { + $orders[] = $direction->expression; + } elseif (is_array($direction)) { + // This is new feature + if (empty($direction)) { + continue; + } + + $fieldValues = []; + foreach($direction as $fieldValue) { + $fieldValues[] = $this->db->quoteValue($fieldValue); + } + + $orders[] = 'FIELD(' . $this->db->quoteColumnName($name) . ',' . implode(',', $fieldValues) . ')'; + // End of new feature + } else { + $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); + } + } + + return 'ORDER BY ' . implode(', ', $orders); + } + +} diff --git a/common/db/mysql/Schema.php b/common/db/mysql/Schema.php new file mode 100644 index 0000000..301d828 --- /dev/null +++ b/common/db/mysql/Schema.php @@ -0,0 +1,12 @@ +db); + } + +} diff --git a/common/models/UsernameHistory.php b/common/models/UsernameHistory.php index c2f832f..720cc10 100644 --- a/common/models/UsernameHistory.php +++ b/common/models/UsernameHistory.php @@ -1,7 +1,6 @@ hasOne(Account::class, ['id' => 'account_id']); } + /** + * @param int $afterTime + * @return UsernameHistory|null + */ + public function findNext(int $afterTime = null) /*: ?UsernameHistory*/ { + return self::find() + ->andWhere(['account_id' => $this->account_id]) + ->andWhere(['>', 'applied_in', $afterTime ?: $this->applied_in]) + ->orderBy(['applied_in' => SORT_ASC]) + ->one(); + } + } diff --git a/console/migrations/m160919_170008_improve_username_history.php b/console/migrations/m160919_170008_improve_username_history.php new file mode 100644 index 0000000..3d7b3f1 --- /dev/null +++ b/console/migrations/m160919_170008_improve_username_history.php @@ -0,0 +1,33 @@ +execute(' + INSERT INTO {{%usernames_history}} (account_id, username, applied_in) + SELECT id as account_id, username, created_at as applied_at + FROM {{%accounts}} + '); + $this->createIndex('applied_in', '{{%usernames_history}}', 'applied_in'); + $this->createIndex('username', '{{%usernames_history}}', 'username'); + } + + public function safeDown() { + $this->dropIndex('applied_in', '{{%usernames_history}}'); + $this->dropIndex('username', '{{%usernames_history}}'); + $this->execute(' + DELETE FROM {{%usernames_history}} + WHERE id IN ( + SELECT t1.id + FROM ( + SELECT id, MIN(applied_in) + FROM {{%usernames_history}} + GROUP BY account_id + ) t1 + ) + '); + } + +} diff --git a/docker/nginx/account.ely.by.conf.template b/docker/nginx/account.ely.by.conf.template index 3670e88..03a4522 100644 --- a/docker/nginx/account.ely.by.conf.template +++ b/docker/nginx/account.ely.by.conf.template @@ -13,16 +13,24 @@ server { set $request_url $request_uri; set $host_with_uri '${host}${request_uri}'; - if ($host_with_uri ~* '^${AUTHSERVER_HOST}/auth') { + rewrite_log on; + error_log /var/log/nginx/error.log debug; + + if ($host_with_uri ~ '^${AUTHSERVER_HOST}/auth') { set $request_url '/api/authserver${request_uri}'; rewrite ^/auth /api/authserver$uri last; } - if ($host_with_uri ~* '^${AUTHSERVER_HOST}/session') { + if ($host_with_uri ~ '^${AUTHSERVER_HOST}/session') { set $request_url '/api/minecraft${request_uri}'; rewrite ^/session /api/minecraft$uri last; } + if ($host_with_uri ~ '^${AUTHSERVER_HOST}/api/(user|profiles)') { + set $request_url '/api/mojang${request_uri}'; + rewrite ^/api/(user|profiles) /api/mojang$uri last; + } + location / { alias $frontend_path; index index.html; diff --git a/tests/codeception/api/_pages/MojangApiRoute.php b/tests/codeception/api/_pages/MojangApiRoute.php new file mode 100644 index 0000000..edf95ac --- /dev/null +++ b/tests/codeception/api/_pages/MojangApiRoute.php @@ -0,0 +1,27 @@ +route = '/mojang/profiles/' . $username; + $params = $at === null ? [] : ['at' => $at]; + $this->actor->sendGET($this->getUrl(), $params); + } + + public function usernamesByUuid($uuid) { + $this->route = "/mojang/profiles/{$uuid}/names"; + $this->actor->sendGET($this->getUrl()); + } + + public function uuidsByUsernames($uuids) { + $this->route = '/mojang/profiles'; + $this->actor->sendPOST($this->getUrl(), $uuids); + } + +} diff --git a/tests/codeception/api/functional/mojang/UsernameToUuidCest.php b/tests/codeception/api/functional/mojang/UsernameToUuidCest.php new file mode 100644 index 0000000..d952f83 --- /dev/null +++ b/tests/codeception/api/functional/mojang/UsernameToUuidCest.php @@ -0,0 +1,67 @@ +route = new MojangApiRoute($I); + } + + public function getUuidByUsername(FunctionalTester $I) { + $I->wantTo('get user uuid by his username'); + $this->route->usernameToUuid('Admin'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'id' => 'df936908b2e1544d96f82977ec213022', + 'name' => 'Admin', + ]); + } + + public function getUuidByUsernameAtMoment(FunctionalTester $I) { + $I->wantTo('get user uuid by his username at fixed moment'); + $this->route->usernameToUuid('klik201', 1474404142); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'id' => 'd6b3e93564664cb886dbb5df91ae6541', + 'name' => 'klik202', + ]); + } + + public function getUuidByUsernameAtWrongMoment(FunctionalTester $I) { + $I->wantTo('get 204 if passed once used, but changed username at moment, when it was changed'); + $this->route->usernameToUuid('klik201', 1474404144); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + + public function getUuidByUsernameWithoutMoment(FunctionalTester $I) { + $I->wantTo('get 204 if username not busy and not passed valid time mark, when it was busy'); + $this->route->usernameToUuid('klik201'); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + + public function getUuidByWrongUsername(FunctionalTester $I) { + $I->wantTo('get user uuid by some wrong username'); + $this->route->usernameToUuid('not-exists-user'); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + + public function nonPassedUsername(FunctionalTester $I) { + $I->wantTo('get 404 on not passed username'); + $this->route->usernameToUuid(''); + $I->canSeeResponseCodeIs(404); + } + +} diff --git a/tests/codeception/api/functional/mojang/UsernamesToUuidsCest.php b/tests/codeception/api/functional/mojang/UsernamesToUuidsCest.php new file mode 100644 index 0000000..caa42dd --- /dev/null +++ b/tests/codeception/api/functional/mojang/UsernamesToUuidsCest.php @@ -0,0 +1,131 @@ +route = new MojangApiRoute($I); + } + + public function geUuidByOneUsername(FunctionalTester $I) { + $I->wantTo('get uuid by one username'); + $this->route->uuidsByUsernames(['Admin']); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + [ + 'id' => 'df936908b2e1544d96f82977ec213022', + 'name' => 'Admin', + ], + ]); + } + + public function getUuidsByUsernames(FunctionalTester $I) { + $I->wantTo('get uuids by few usernames'); + $this->route->uuidsByUsernames(['Admin', 'AccWithOldPassword', 'Notch']); + $this->validateFewValidUsernames($I); + } + + public function getUuidsByUsernamesWithPostString(FunctionalTester $I) { + $I->wantTo('get uuids by few usernames'); + $this->route->uuidsByUsernames(json_encode(['Admin', 'AccWithOldPassword', 'Notch'])); + $this->validateFewValidUsernames($I); + } + + public function getUuidsByPartialNonexistentUsernames(FunctionalTester $I) { + $I->wantTo('get uuids by few usernames and some nonexistent'); + $this->route->uuidsByUsernames(['Admin', 'not-exists-user']); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + [ + 'id' => 'df936908b2e1544d96f82977ec213022', + 'name' => 'Admin', + ], + ]); + } + + public function passAllNonexistentUsernames(FunctionalTester $I) { + $I->wantTo('get specific response when pass all nonexistent usernames'); + $this->route->uuidsByUsernames(['not-exists-1', 'not-exists-2']); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([]); + } + + public function passTooManyUsernames(FunctionalTester $I) { + $I->wantTo('get specific response when pass too many usernames'); + $usernames = []; + for($i = 0; $i < 150; $i++) { + $usernames[] = random_bytes(10); + } + $this->route->uuidsByUsernames($usernames); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'Not more that 100 profile name per call is allowed.', + ]); + } + + public function passEmptyUsername(FunctionalTester $I) { + $I->wantTo('get specific response when pass empty username'); + $this->route->uuidsByUsernames(['Admin', '']); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'profileName can not be null, empty or array key.', + ]); + } + + public function passEmptyField(FunctionalTester $I) { + $I->wantTo('get response when pass empty array'); + $this->route->uuidsByUsernames([]); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'Passed array of profile names is an invalid JSON string.', + ]); + } + + public function passWrongPostBody(FunctionalTester $I) { + $I->wantTo('get specific response when pass invalid json string'); + $this->route->uuidsByUsernames('wrong-json'); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'Passed array of profile names is an invalid JSON string.', + ]); + } + + private function validateFewValidUsernames(FunctionalTester $I) { + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + [ + 'id' => 'df936908b2e1544d96f82977ec213022', + 'name' => 'Admin', + ], + [ + 'id' => 'bdc239f08a22518d8b93f02d4827c3eb', + 'name' => 'AccWithOldPassword', + ], + [ + 'id' => '4aaf4f003b5b4d3692529e8ee0c86679', + 'name' => 'Notch', + ], + ]); + } + +} diff --git a/tests/codeception/api/functional/mojang/UuidToUsernamesHistoryCest.php b/tests/codeception/api/functional/mojang/UuidToUsernamesHistoryCest.php new file mode 100644 index 0000000..ae98423 --- /dev/null +++ b/tests/codeception/api/functional/mojang/UuidToUsernamesHistoryCest.php @@ -0,0 +1,69 @@ +route = new MojangApiRoute($I); + } + + public function getUsernameByUuid(FunctionalTester $I) { + $I->wantTo('get usernames history by uuid for user, without history'); + $this->route->usernamesByUuid('df936908b2e1544d96f82977ec213022'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + [ + 'name' => 'Admin', + ], + ]); + } + + public function getUsernameByUuidWithHistory(FunctionalTester $I) { + $I->wantTo('get usernames history by dashed uuid and expect history with time marks'); + $this->route->usernamesByUuid('d6b3e935-6466-4cb8-86db-b5df91ae6541'); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + [ + 'name' => 'klik202', + ], + [ + 'name' => 'klik201', + 'changedToAt' => 1474404141000, + ], + [ + 'name' => 'klik202', + 'changedToAt' => 1474404143000, + ], + ]); + } + + public function passWrongUuid(FunctionalTester $I) { + $I->wantTo('get user username by some wrong uuid'); + $this->route->usernamesByUuid(Uuid::uuid()); + $I->canSeeResponseCodeIs(204); + $I->canSeeResponseEquals(''); + } + + public function passWrongUuidFormat(FunctionalTester $I) { + $I->wantTo('call profile route with invalid uuid string'); + $this->route->usernamesByUuid('bla-bla-bla'); + $I->canSeeResponseCodeIs(400); + $I->canSeeResponseIsJson(); + $I->canSeeResponseContainsJson([ + 'error' => 'IllegalArgumentException', + 'errorMessage' => 'Invalid uuid format.', + ]); + } + +} diff --git a/tests/codeception/api/unit/models/authentication/RegistrationFormTest.php b/tests/codeception/api/unit/models/authentication/RegistrationFormTest.php index 6159731..0a1313f 100644 --- a/tests/codeception/api/unit/models/authentication/RegistrationFormTest.php +++ b/tests/codeception/api/unit/models/authentication/RegistrationFormTest.php @@ -6,6 +6,7 @@ use api\models\authentication\RegistrationForm; use Codeception\Specify; use common\models\Account; use common\models\EmailActivation; +use common\models\UsernameHistory; use tests\codeception\api\unit\DbTestCase; use tests\codeception\common\fixtures\AccountFixture; use Yii; @@ -118,6 +119,11 @@ class RegistrationFormTest extends DbTestCase { 'account_id' => $account->id, 'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION, ])->exists())->true(); + expect('username history record exists in database', UsernameHistory::find()->andWhere([ + 'username' => $account->username, + 'account_id' => $account->id, + 'applied_in' => $account->created_at, + ])->exists())->true(); expect_file('message file exists', $this->getMessageFile())->exists(); } diff --git a/tests/codeception/common/fixtures/data/accounts.php b/tests/codeception/common/fixtures/data/accounts.php index 00bf8f9..33db104 100644 --- a/tests/codeception/common/fixtures/data/accounts.php +++ b/tests/codeception/common/fixtures/data/accounts.php @@ -133,4 +133,17 @@ return [ 'created_at' => 1472682343, 'updated_at' => 1472682343, ], + 'account-with-usernames-history' => [ + 'id' => 11, + 'uuid' => 'd6b3e935-6466-4cb8-86db-b5df91ae6541', + 'username' => 'klik202', + 'email' => 'klik202@mail.ru', + 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 + 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, + 'lang' => 'ru', + 'status' => \common\models\Account::STATUS_ACTIVE, + 'rules_agreement_version' => \common\LATEST_RULES_VERSION, + 'created_at' => 1474404139, + 'updated_at' => 1474404149, + ], ]; diff --git a/tests/codeception/common/fixtures/data/usernames-history.php b/tests/codeception/common/fixtures/data/usernames-history.php index bee73cf..26de303 100644 --- a/tests/codeception/common/fixtures/data/usernames-history.php +++ b/tests/codeception/common/fixtures/data/usernames-history.php @@ -1,4 +1,27 @@ 1, + 'username' => 'Admin', + 'account_id' => 1, + 'applied_in' => 1451775316, + ], + [ + 'id' => 2, + 'username' => 'klik202', + 'account_id' => 11, + 'applied_in' => 1474404139, + ], + [ + 'id' => 3, + 'username' => 'klik201', + 'account_id' => 11, + 'applied_in' => 1474404141, + ], + [ + 'id' => 4, + 'username' => 'klik202', + 'account_id' => 11, + 'applied_in' => 1474404143, + ], ]; diff --git a/tests/codeception/common/unit/db/mysql/QueryBuilderTest.php b/tests/codeception/common/unit/db/mysql/QueryBuilderTest.php new file mode 100644 index 0000000..77a3d96 --- /dev/null +++ b/tests/codeception/common/unit/db/mysql/QueryBuilderTest.php @@ -0,0 +1,16 @@ +db); + $result = $queryBuilder->buildOrderBy(['dummy' => ['first', 'second']]); + $this->assertEquals("ORDER BY FIELD(`dummy`,'first','second')", $result); + } + +} diff --git a/tests/codeception/config/api/config.php b/tests/codeception/config/api/config.php index ff84813..eb1ccbb 100644 --- a/tests/codeception/config/api/config.php +++ b/tests/codeception/config/api/config.php @@ -11,7 +11,7 @@ return [ ], 'modules' => [ 'authserver' => [ - 'baseDomain' => 'http://localhost', + 'host' => 'localhost', ], ], 'params' => [