Merge branch 'develop' into sentry

This commit is contained in:
ErickSkrauch 2016-12-07 21:36:49 +03:00
commit 4623063074
33 changed files with 424 additions and 233 deletions

View File

@ -1,21 +1,57 @@
# Основные параметры # Параметры приложения
## Env приложения
YII_DEBUG=true YII_DEBUG=true
YII_ENV=dev YII_ENV=dev
## Параметры, отвечающие за безопасность
JWT_USER_SECRET= JWT_USER_SECRET=
## Внешние сервисы
RECAPTCHA_PUBLIC= RECAPTCHA_PUBLIC=
RECAPTCHA_SECRET= RECAPTCHA_SECRET=
SENTRY_DSN= SENTRY_DSN=
## SMTP параметры
SMTP_USER=
SMTP_PASS=
SMTP_PORT=
## Параметры подключения к базе данных
DB_HOST=db
DB_DATABASE=ely_accounts
DB_USER=ely_accounts_user
DB_PASSWORD=ely_accounts_password
## Параметры подключения к redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=
## Параметры подключения к redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASS=
## Параметры подключения к rabbitmq
RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USER=ely-accounts-app
RABBITMQ_PASS=ely-accounts-app-password
RABBITMQ_VHOST=/ely.by
## Конфигурация для Dev.
XDEBUG_CONFIG=remote_host=10.254.254.254
PHP_IDE_CONFIG=serverName=docker
# Web # Web
VIRTUAL_HOST=account.ely.by,authserver.ely.by VIRTUAL_HOST=account.ely.by,authserver.ely.by
AUTHSERVER_HOST=authserver.ely.by AUTHSERVER_HOST=authserver.ely.by
# LETSENCRYPT_HOST=account.ely.by # LETSENCRYPT_HOST=account.ely.by
# LETSENCRYPT_EMAIL=erickskrauch@ely.by # LETSENCRYPT_EMAIL=erickskrauch@ely.by
# SMTP (только для production)
SMTP_USER=
SMTP_PASS=
# MySQL # MySQL
MYSQL_ALLOW_EMPTY_PASSWORD=yes MYSQL_ALLOW_EMPTY_PASSWORD=yes
MYSQL_ROOT_PASSWORD= MYSQL_ROOT_PASSWORD=
@ -27,7 +63,3 @@ MYSQL_PASSWORD=ely_accounts_password
RABBITMQ_DEFAULT_USER=ely-accounts-app RABBITMQ_DEFAULT_USER=ely-accounts-app
RABBITMQ_DEFAULT_PASS=ely-accounts-app-password RABBITMQ_DEFAULT_PASS=ely-accounts-app-password
RABBITMQ_DEFAULT_VHOST=/ely.by RABBITMQ_DEFAULT_VHOST=/ely.by
# Конфигурация для Dev.
XDEBUG_CONFIG=remote_host=10.254.254.254
PHP_IDE_CONFIG=serverName=docker

View File

@ -4,21 +4,40 @@ stages:
- release - release
variables: variables:
CONTAINER_IMAGE: registry.ely.by/elyby/accounts DOCKER_DRIVER: aufs
CONTAINER_IMAGE: "registry.ely.by/elyby/accounts"
test:backend: test:backend:
image: jonaskello/docker-and-compose:1.12.1-1.8.0 image: docker:latest
services: services:
- docker:1.12.1-dind - mariadb:10.0
- redis:3.0-alpine
variables:
# mariadb config
MYSQL_RANDOM_ROOT_PASSWORD: "true"
MYSQL_DATABASE: "ely_accounts_test"
MYSQL_USER: "ely_accounts_tester"
MYSQL_PASSWORD: "ely_accounts_tester_password"
stage: test stage: test
before_script: before_script:
- docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by - docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by
- echo "$SSH_PRIVATE_KEY" > id_rsa - echo "$SSH_PRIVATE_KEY" > id_rsa
- docker-compose -f tests/docker-compose.yml build --pull testphp
after_script:
- docker-compose -f tests/docker-compose.yml down -v
script: script:
- docker-compose -f tests/docker-compose.yml run --rm testphp ./vendor/bin/codecept run -c tests - export TEMP_DEV_IMAGE="${CONTAINER_IMAGE}:ci-${CI_BUILD_ID}"
- docker build --pull -f Dockerfile-dev -t $TEMP_DEV_IMAGE .
- >
docker run --rm
--add-host=mariadb:`getent hosts mariadb | awk '{ print $1 ; exit }'`
--add-host=redis:`getent hosts redis | awk '{ print $1 ; exit }'`
-e YII_DEBUG="true"
-e YII_ENV="test"
-e DB_HOST="mariadb"
-e DB_DATABASE="ely_accounts_test"
-e DB_USER="ely_accounts_tester"
-e DB_PASSWORD="ely_accounts_tester_password"
-e REDIS_HOST="redis"
$TEMP_DEV_IMAGE
php vendor/bin/codecept run -c tests
test:frontend: test:frontend:
image: node:5.12 image: node:5.12
@ -28,8 +47,8 @@ test:frontend:
- frontend/node_modules - frontend/node_modules
script: script:
- cd frontend - cd frontend
- npm i --silent - npm i --silent > /dev/null
- npm run test - npm run test --silent
build:production: build:production:
image: docker:latest image: docker:latest

View File

@ -1,4 +1,4 @@
FROM registry.ely.by/elyby/accounts-php:1.1.2 FROM registry.ely.by/elyby/accounts-php:1.2.0
# Вносим конфигурации для крона и воркеров # Вносим конфигурации для крона и воркеров
COPY docker/cron/* /etc/cron.d/ COPY docker/cron/* /etc/cron.d/
@ -32,7 +32,7 @@ COPY ./frontend/scripts /var/www/frontend/scripts
COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils
RUN cd ../frontend \ RUN cd ../frontend \
&& npm install \ && npm install --quiet --depth -1 \
&& cd - && cd -
# Удаляем ключи из production контейнера на всякий случай # Удаляем ключи из production контейнера на всякий случай
@ -46,7 +46,7 @@ RUN mkdir -p api/runtime api/web/assets console/runtime \
# Билдим фронт # Билдим фронт
&& cd frontend \ && cd frontend \
&& ln -s /var/www/frontend/node_modules $PWD/node_modules \ && ln -s /var/www/frontend/node_modules $PWD/node_modules \
&& npm run build \ && npm run build:quite --quiet \
&& rm node_modules \ && rm node_modules \
# Копируем билд наружу, чтобы его не затёрло volume в dev режиме # Копируем билд наружу, чтобы его не затёрло volume в dev режиме
&& cp -r ./dist /var/www/dist \ && cp -r ./dist /var/www/dist \

View File

@ -1,4 +1,4 @@
FROM registry.ely.by/elyby/accounts-php:1.1.2-dev FROM registry.ely.by/elyby/accounts-php:1.2.0-dev
# Вносим конфигурации для крона и воркеров # Вносим конфигурации для крона и воркеров
COPY docker/cron/* /etc/cron.d/ COPY docker/cron/* /etc/cron.d/
@ -32,7 +32,7 @@ COPY ./frontend/scripts /var/www/frontend/scripts
COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils
RUN cd ../frontend \ RUN cd ../frontend \
&& npm install \ && npm install --quiet --depth -1 \
&& cd - && cd -
# Наконец переносим все сорцы внутрь контейнера # Наконец переносим все сорцы внутрь контейнера
@ -43,7 +43,7 @@ RUN mkdir -p api/runtime api/web/assets console/runtime \
# Билдим фронт # Билдим фронт
&& cd frontend \ && cd frontend \
&& ln -s /var/www/frontend/node_modules $PWD/node_modules \ && ln -s /var/www/frontend/node_modules $PWD/node_modules \
&& npm run build \ && npm run build:quite --quiet \
&& rm node_modules \ && rm node_modules \
# Копируем билд наружу, чтобы его не затёрло volume в dev режиме # Копируем билд наружу, чтобы его не затёрло volume в dev режиме
&& cp -r ./dist /var/www/dist \ && cp -r ./dist /var/www/dist \

View File

@ -1,6 +1,7 @@
<?php <?php
namespace api\controllers; namespace api\controllers;
use api\filters\NginxCache;
use Yii; use Yii;
use yii\helpers\ArrayHelper; use yii\helpers\ArrayHelper;
@ -11,6 +12,12 @@ class OptionsController extends Controller {
'authenticator' => [ 'authenticator' => [
'except' => ['index'], 'except' => ['index'],
], ],
'nginxCache' => [
'class' => NginxCache::class,
'rules' => [
'index' => 3600, // 1h
],
],
]); ]);
} }

View File

@ -0,0 +1,35 @@
<?php
namespace api\filters;
use Yii;
use yii\base\ActionFilter;
class NginxCache extends ActionFilter {
/**
* @var array|callable массив или callback, содержащий пары роут -> сколько кэшировать.
*
* Период можно задавать 2-умя путями:
* - если значение начинается с префикса @, оно задаёт абсолютное время в unix timestamp,
* до которого ответ может быть закэширован.
* - в ином случае значение интерпретируется как количество секунд, на которое необходимо
* закэшировать ответ
*/
public $rules;
public function afterAction($action, $result) {
$rule = $this->rules[$action->id] ?? null;
if ($rule !== null) {
if (is_callable($rule)) {
$cacheTime = $rule($action);
} else {
$cacheTime = $rule;
}
Yii::$app->response->headers->set('X-Accel-Expires', $cacheTime);
}
return parent::afterAction($action, $result);
}
}

View File

@ -28,15 +28,19 @@ class ChangeUsernameForm extends ApiForm {
]; ];
} }
public function change() { public function change() : bool {
if (!$this->validate()) { if (!$this->validate()) {
return false; return false;
} }
$transaction = Yii::$app->db->beginTransaction();
$account = $this->getAccount(); $account = $this->getAccount();
$oldNickname = $account->username; if ($this->username === $account->username) {
return true;
}
$transaction = Yii::$app->db->beginTransaction();
try { try {
$oldNickname = $account->username;
$account->username = $this->username; $account->username = $this->username;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot save account model with new username'); throw new ErrorException('Cannot save account model with new username');

View File

@ -6,22 +6,5 @@ return [
'schemaCacheDuration' => 3600, 'schemaCacheDuration' => 3600,
'schemaCache' => 'cache', 'schemaCache' => 'cache',
], ],
'mailer' => [
'useFileTransport' => false,
'transport' => [
'class' => Swift_SmtpTransport::class,
'host' => 'ely.by',
'username' => getenv('SMTP_USER'),
'password' => getenv('SMTP_PASS'),
'port' => 587,
'encryption' => 'tls',
'streamOptions' => [
'ssl' => [
'allow_self_signed' => true,
'verify_peer' => false,
],
],
],
],
], ],
]; ];

View File

@ -8,9 +8,9 @@ return [
], ],
'db' => [ 'db' => [
'class' => yii\db\Connection::class, 'class' => yii\db\Connection::class,
'dsn' => 'mysql:host=db;dbname=' . getenv('MYSQL_DATABASE'), 'dsn' => 'mysql:host=' . (getenv('DB_HOST') ?: 'db') . ';dbname=' . getenv('DB_DATABASE'),
'username' => getenv('MYSQL_USER'), 'username' => getenv('DB_USER'),
'password' => getenv('MYSQL_PASSWORD'), 'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8', 'charset' => 'utf8',
'schemaMap' => [ 'schemaMap' => [
'mysql' => common\db\mysql\Schema::class, 'mysql' => common\db\mysql\Schema::class,
@ -19,6 +19,20 @@ return [
'mailer' => [ 'mailer' => [
'class' => yii\swiftmailer\Mailer::class, 'class' => yii\swiftmailer\Mailer::class,
'viewPath' => '@common/mail', 'viewPath' => '@common/mail',
'transport' => [
'class' => Swift_SmtpTransport::class,
'host' => 'ely.by',
'username' => getenv('SMTP_USER'),
'password' => getenv('SMTP_PASS'),
'port' => getenv('SMTP_PORT') ?: 587,
'encryption' => 'tls',
'streamOptions' => [
'ssl' => [
'allow_self_signed' => true,
'verify_peer' => false,
],
],
],
], ],
'sentry' => [ 'sentry' => [
'class' => mito\sentry\SentryComponent::class, 'class' => mito\sentry\SentryComponent::class,
@ -35,18 +49,18 @@ return [
], ],
'redis' => [ 'redis' => [
'class' => common\components\Redis\Connection::class, 'class' => common\components\Redis\Connection::class,
'hostname' => 'redis', 'hostname' => getenv('REDIS_HOST') ?: 'redis',
'password' => null, 'password' => getenv('REDIS_PASS') ?: null,
'port' => 6379, 'port' => getenv('REDIS_PORT') ?: 6379,
'database' => 0, 'database' => getenv('REDIS_DATABASE') ?: 0,
], ],
'amqp' => [ 'amqp' => [
'class' => common\components\RabbitMQ\Component::class, 'class' => common\components\RabbitMQ\Component::class,
'host' => 'rabbitmq', 'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq',
'port' => 5672, 'port' => getenv('RABBITMQ_PORT') ?: 5672,
'user' => getenv('RABBITMQ_DEFAULT_USER'), 'user' => getenv('RABBITMQ_USER'),
'password' => getenv('RABBITMQ_DEFAULT_PASS'), 'password' => getenv('RABBITMQ_PASS'),
'vhost' => getenv('RABBITMQ_DEFAULT_VHOST'), 'vhost' => getenv('RABBITMQ_VHOST'),
], ],
'guzzle' => [ 'guzzle' => [
'class' => GuzzleHttp\Client::class, 'class' => GuzzleHttp\Client::class,

View File

@ -15,7 +15,7 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"require": { "require": {
"php": "^7.0.6", "php": "^7.0.6",
"yiisoft/yii2": "2.0.9", "yiisoft/yii2": "2.0.10",
"yiisoft/yii2-swiftmailer": "*", "yiisoft/yii2-swiftmailer": "*",
"ramsey/uuid": "^3.5.0", "ramsey/uuid": "^3.5.0",
"league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda", "league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda",

View File

@ -13,7 +13,7 @@ services:
env_file: .env env_file: .env
web: web:
build: ./docker/nginx image: registry.ely.by/elyby/accounts-nginx:latest
volumes_from: volumes_from:
- app - app
links: links:

View File

@ -9,7 +9,7 @@ services:
env_file: .env env_file: .env
web: web:
build: ./docker/nginx image: registry.ely.by/elyby/accounts-nginx:1.0.2
volumes_from: volumes_from:
- app - app
links: links:

0
docker/cron/.gitkeep Normal file
View File

View File

@ -1,2 +0,0 @@
# https://crontab.guru/every-hour
0 * * * * root /usr/local/bin/php /var/www/html/yii cleanup/access-tokens >/dev/null 2>&1

View File

@ -1,11 +0,0 @@
FROM nginx:1.11-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY account.ely.by.conf.template /etc/nginx/conf.d/account.ely.by.conf.template
COPY run.sh /run.sh
RUN rm /etc/nginx/conf.d/default.conf \
&& chmod a+x /run.sh
ENTRYPOINT ["/run.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,78 +0,0 @@
server {
listen 80;
root $root_path;
charset utf-8;
index index.html;
etag on;
# Это можно раскоментить для целей отладки
# rewrite_log on;
# error_log /var/log/nginx/error.log debug;
set $root_path '/var/www/html';
set $frontend_path '${root_path}/frontend/dist';
set $request_url $request_uri;
set $host_with_uri '${host}${request_uri}';
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') {
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;
try_files $uri /index.html =404;
}
location /api {
try_files $uri $uri /api/web/index.php$is_args$args;
}
location ~* \.php$ {
fastcgi_pass php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SERVER_NAME $host;
fastcgi_param REQUEST_URI $request_url;
fastcgi_param REMOTE_ADDR $http_x_real_ip;
try_files $uri =404;
}
# html файлы идут отдельно, для них будет применяться E-Tag кэширование
location ~* \.html$ {
root $frontend_path;
access_log off;
}
# Раздача статики для frontend с указанием max-кэша. Сброс будет по #hash после ребилда webpackом
location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|json|css|zip|rar|eot|ttf|woff|woff2|ico)$ {
root $frontend_path;
expires max;
etag off;
access_log off;
}
# Запросы к статике для email, их нужно запустить внутрь vendor
location ^~ /images/emails/assets {
rewrite ^/images/emails/assets/(.+)$ /vendor/ely/emails-renderer/dist/assets/$1 last;
}
location ^~ /vendor/ely/emails-renderer/dist/assets {
alias '${root_path}/vendor/ely/email-renderer/dist/assets';
try_files $uri =404;
}
}

View File

@ -1,25 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 10;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -1,5 +0,0 @@
#!/usr/bin/env sh
envsubst '$AUTHSERVER_HOST' < /etc/nginx/conf.d/account.ely.by.conf.template > /etc/nginx/conf.d/default.conf
exec "$@"

View File

@ -1,5 +1,6 @@
namespace: tests\codeception\api namespace: tests\codeception\api
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@ -4,23 +4,16 @@ modules:
- Filesystem - Filesystem
- Yii2 - Yii2
- tests\codeception\common\_support\FixtureHelper - tests\codeception\common\_support\FixtureHelper
- tests\codeception\common\_support\amqp\Helper
- Redis - Redis
- AMQP
- Asserts - Asserts
- REST: - REST:
depends: Yii2 depends: Yii2
config: config:
Yii2: Yii2:
configFile: '../config/api/functional.php' configFile: '../config/api/functional.php'
cleanup: true cleanup: false
Redis: Redis:
host: testredis host: "%REDIS_HOST%"
port: 6379 port: 6379
database: 0 database: 0
AMQP:
host: testrabbit
port: 5672
username: 'ely-accounts-tester'
password: 'tester-password'
vhost: '/account.ely.by/tests'
queues: ['account-operations']

View File

@ -3,6 +3,8 @@ modules:
enabled: enabled:
- Yii2: - Yii2:
part: [orm, email, fixtures] part: [orm, email, fixtures]
- tests\codeception\common\_support\amqp\Helper
config: config:
Yii2: Yii2:
configFile: '../config/api/unit.php' configFile: '../config/api/unit.php'
cleanup: false

View File

@ -0,0 +1,57 @@
<?php
namespace tests\codeception\api\unit\filters;
use api\filters\NginxCache;
use tests\codeception\api\unit\TestCase;
use Yii;
use yii\base\Action;
use yii\web\Controller;
use yii\web\HeaderCollection;
use yii\web\Request;
class NginxCacheTest extends TestCase {
public function testAfterAction() {
$this->testAfterActionInternal(3600, 3600);
$this->testAfterActionInternal('@' . (time() + 30), '@' . (time() + 30));
$this->testAfterActionInternal(function() {
return 3000;
}, 3000);
}
private function testAfterActionInternal($ruleConfig, $expected) {
/** @var HeaderCollection|\PHPUnit_Framework_MockObject_MockObject $headers */
$headers = $this->getMockBuilder(HeaderCollection::class)
->setMethods(['set'])
->getMock();
$headers->expects($this->once())
->method('set')
->with('X-Accel-Expires', $expected);
/** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */
$request = $this->getMockBuilder(Request::class)
->setMethods(['getHeaders'])
->getMock();
$request->expects($this->any())
->method('getHeaders')
->willReturn($headers);
Yii::$app->set('response', $request);
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */
$controller = $this->getMockBuilder(Controller::class)
->setConstructorArgs(['mock', Yii::$app])
->getMock();
$component = new NginxCache([
'rules' => [
'index' => $ruleConfig,
],
]);
$component->afterAction(new Action('index', $controller), '');
}
}

View File

@ -25,9 +25,15 @@ class ConfirmEmailFormTest extends TestCase {
$this->assertInstanceOf(AccountSession::class, $result->getSession(), 'session was generated'); $this->assertInstanceOf(AccountSession::class, $result->getSession(), 'session was generated');
$activationExists = EmailActivation::find()->andWhere(['key' => $fixture['key']])->exists(); $activationExists = EmailActivation::find()->andWhere(['key' => $fixture['key']])->exists();
$this->assertFalse($activationExists, 'email activation key is not exist'); $this->assertFalse($activationExists, 'email activation key is not exist');
/** @var Account $user */ /** @var Account $account */
$user = Account::findOne($fixture['account_id']); $account = Account::findOne($fixture['account_id']);
$this->assertEquals(Account::STATUS_ACTIVE, $user->status, 'user status changed to active'); $this->assertEquals(Account::STATUS_ACTIVE, $account->status, 'user status changed to active');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals($account->id, $body['accountId']);
$this->assertEquals($account->username, $body['newUsername']);
$this->assertNull($body['oldUsername']);
} }
private function createModel($key) { private function createModel($key) {

View File

@ -18,9 +18,8 @@ class ConfirmNewEmailFormTest extends TestCase {
} }
public function testChangeEmail() { public function testChangeEmail() {
$accountId = $this->tester->grabFixture('accounts', 'account-with-change-email-finish-state')['id'];
/** @var Account $account */ /** @var Account $account */
$account = Account::findOne($accountId); $account = Account::findOne($this->getAccountId());
$newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation'); $newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation');
$model = new ConfirmNewEmailForm($account, [ $model = new ConfirmNewEmailForm($account, [
'key' => $newEmailConfirmationFixture['key'], 'key' => $newEmailConfirmationFixture['key'],
@ -32,6 +31,23 @@ class ConfirmNewEmailFormTest extends TestCase {
])); ]));
$data = unserialize($newEmailConfirmationFixture['_data']); $data = unserialize($newEmailConfirmationFixture['_data']);
$this->assertEquals($data['newEmail'], $account->email); $this->assertEquals($data['newEmail'], $account->email);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
/** @var Account $account */
$account = Account::findOne($this->getAccountId());
$model = new ConfirmNewEmailForm($account);
$model->createTask(1, 'test1@ely.by', 'test@ely.by');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1@ely.by', $body['newEmail']);
$this->assertEquals('test@ely.by', $body['oldEmail']);
}
private function getAccountId() {
return $this->tester->grabFixture('accounts', 'account-with-change-email-finish-state')['id'];
} }
} }

View File

@ -35,6 +35,7 @@ class ChangeUsernameFormTest extends TestCase {
$this->assertTrue($model->change()); $this->assertTrue($model->change());
$this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username); $this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username);
$this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname'])); $this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname']));
$this->tester->canSeeAmqpMessageIsCreated('events');
} }
public function testChangeWithoutChange() { public function testChangeWithoutChange() {
@ -49,7 +50,8 @@ class ChangeUsernameFormTest extends TestCase {
'AND', 'AND',
'username' => $username, 'username' => $username,
['>=', 'applied_in', $callTime], ['>=', 'applied_in', $callTime],
]), 'no new UsernameHistory record, if we don\'t change nickname'); ]), 'no new UsernameHistory record, if we don\'t change username');
$this->tester->cantSeeAmqpMessageIsCreated('events');
} }
public function testChangeCase() { public function testChangeCase() {
@ -65,13 +67,17 @@ class ChangeUsernameFormTest extends TestCase {
UsernameHistory::findOne(['username' => $newUsername]), UsernameHistory::findOne(['username' => $newUsername]),
'username should change, if we change case of some letters' 'username should change, if we change case of some letters'
); );
$this->tester->canSeeAmqpMessageIsCreated('events');
} }
public function testCreateTask() { public function testCreateTask() {
$model = new ChangeUsernameForm(); $model = new ChangeUsernameForm();
$model->createEventTask('1', 'test1', 'test'); $model->createEventTask(1, 'test1', 'test');
// TODO: у меня пока нет идей о том, чтобы это как-то успешно протестировать, увы $message = $this->tester->grabLastSentAmqpMessage('events');
// но по крайней мере можно убедиться, что оно не падает где-то на этом шаге $body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1', $body['newUsername']);
$this->assertEquals('test', $body['oldUsername']);
} }
private function getAccountId() { private function getAccountId() {

View File

@ -0,0 +1,94 @@
<?php
namespace tests\codeception\common\_support\amqp;
use Codeception\Exception\ModuleException;
use Codeception\Module;
use Codeception\Module\Yii2;
class Helper extends Module {
/**
* Checks that message is created.
*
* ```php
* <?php
* // check that at least 1 message was created
* $I->seeAmqpMessageIsCreated();
*
* // check that only 3 messages were created
* $I->seeAmqpMessageIsCreated(3);
* ```
*
* @param string|null $exchange
* @param int|null $num
*/
public function seeAmqpMessageIsCreated($exchange = null, $num = null) {
if ($num === null) {
$this->assertNotEmpty($this->grabSentAmqpMessages($exchange), 'message were created');
return;
}
// TODO: заменить на assertCount() после релиза Codeception 2.2.7
// https://github.com/Codeception/Codeception/pull/3802
/** @noinspection PhpUnitTestsInspection */
$this->assertEquals(
$num,
count($this->grabSentAmqpMessages($exchange)),
'number of created messages is equal to ' . $num
);
}
/**
* Checks that no messages was created
*
* @param string|null $exchange
*/
public function dontSeeAmqpMessageIsCreated($exchange = null) {
$this->seeAmqpMessageIsCreated($exchange, 0);
}
/**
* Returns last sent message
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage
*/
public function grabLastSentAmqpMessage($exchange = null) {
$this->seeAmqpMessageIsCreated();
$messages = $this->grabSentAmqpMessages($exchange);
return end($messages);
}
/**
* Returns array of all sent amqp messages.
* Each message is `\PhpAmqpLib\Message\AMQPMessage` instance.
* Useful to perform additional checks using `Asserts` module.
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage[]
* @throws ModuleException
*/
public function grabSentAmqpMessages($exchange = null) {
$amqp = $this->grabComponent('amqp');
if (!$amqp instanceof TestComponent) {
throw new ModuleException($this, 'AMQP module is not mocked, can\'t test messages');
}
return $amqp->getSentMessages($exchange);
}
private function grabComponent(string $component) {
return $this->getYii2()->grabComponent($component);
}
private function getYii2() : Yii2 {
$yii2 = $this->getModule('Yii2');
if (!$yii2 instanceof Yii2) {
throw new ModuleException($this, 'Yii2 module must be configured');
}
return $yii2;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace tests\codeception\common\_support\amqp;
use common\components\RabbitMQ\Component;
use PhpAmqpLib\Connection\AbstractConnection;
class TestComponent extends Component {
private $sentMessages = [];
public function init() {
\yii\base\Component::init();
}
public function getConnection() {
/** @noinspection MagicMethodsValidityInspection */
/** @noinspection PhpMissingParentConstructorInspection */
return new class extends AbstractConnection {
public function __construct(
$user,
$password,
$vhost,
$insist,
$login_method,
$login_response,
$locale,
\PhpAmqpLib\Wire\IO\AbstractIO $io,
$heartbeat
) {
// ничего не делаем
}
};
}
public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) {
$this->sentMessages[$exchangeName][] = $this->prepareMessage($message);
}
/**
* @param string|null $exchangeName
* @return \PhpAmqpLib\Message\AMQPMessage[]
*/
public function getSentMessages(string $exchangeName = null) : array {
if ($exchangeName !== null) {
return $this->sentMessages[$exchangeName] ?? [];
} else {
$messages = [];
foreach($this->sentMessages as $exchangeGroup) {
foreach ($exchangeGroup as $message) {
$messages[] = $message;
}
}
return $messages;
}
}
}

View File

@ -1,5 +1,6 @@
namespace: tests\codeception\common namespace: tests\codeception\common
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@ -6,3 +6,4 @@ modules:
config: config:
Yii2: Yii2:
configFile: '../config/common/unit.php' configFile: '../config/common/unit.php'
cleanup: false

View File

@ -10,30 +10,16 @@ return [
], ],
], ],
'components' => [ 'components' => [
'db' => [
'dsn' => 'mysql:host=testdb;dbname=ely_accounts_test',
'username' => 'ely_accounts_tester',
'password' => 'ely_accounts_tester_password',
],
'mailer' => [
'useFileTransport' => true,
],
'urlManager' => [ 'urlManager' => [
'showScriptName' => true, 'showScriptName' => true,
], ],
'redis' => [
'hostname' => 'testredis',
],
'amqp' => [
'host' => 'testrabbit',
'user' => 'ely-accounts-tester',
'password' => 'tester-password',
'vhost' => '/account.ely.by/tests',
],
'security' => [ 'security' => [
// Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается // Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается
'passwordHashCost' => 4, 'passwordHashCost' => 4,
], ],
'amqp' => [
'class' => tests\codeception\common\_support\amqp\TestComponent::class,
],
'sentry' => [ 'sentry' => [
'enabled' => false, 'enabled' => false,
], ],

View File

@ -1,5 +1,6 @@
namespace: tests\codeception\console namespace: tests\codeception\console
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@ -6,3 +6,4 @@ modules:
config: config:
Yii2: Yii2:
configFile: '../config/console/unit.php' configFile: '../config/console/unit.php'
cleanup: false

View File

@ -9,18 +9,21 @@ services:
depends_on: depends_on:
- testdb - testdb
- testredis - testredis
- testrabbit
volumes: volumes:
- ./codeception/_output:/var/www/html/tests/codeception/_output - ./..:/var/www/html
- ./codeception/api/_output:/var/www/html/tests/codeception/api/_output
- ./codeception/common/_output:/var/www/html/tests/codeception/common/_output
- ./codeception/console/_output:/var/www/html/tests/codeception/console/_output
environment: environment:
- YII_DEBUG=true YII_DEBUG: "true"
- YII_ENV=test YII_ENV: "test"
# DB config
DB_HOST: "testdb"
DB_DATABASE: "ely_accounts_test"
DB_USER: "ely_accounts_tester"
DB_PASSWORD: "ely_accounts_tester_password"
# Redis config
REDIS_HOST: "testredis"
# Это я потом, когда-нибудь, уберу # Это я потом, когда-нибудь, уберу
- XDEBUG_CONFIG=remote_host=10.254.254.254 XDEBUG_CONFIG: "remote_host=10.254.254.254"
- PHP_IDE_CONFIG=serverName=docker PHP_IDE_CONFIG: "serverName=docker"
testdb: testdb:
container_name: accountelyby_testdb container_name: accountelyby_testdb
@ -36,11 +39,3 @@ services:
testredis: testredis:
container_name: accountelyby_testredis container_name: accountelyby_testredis
image: redis:3.0-alpine image: redis:3.0-alpine
testrabbit:
container_name: accountelyby_testrabbit
image: rabbitmq:3.6
environment:
RABBITMQ_DEFAULT_USER: "ely-accounts-tester"
RABBITMQ_DEFAULT_PASS: "tester-password"
RABBITMQ_DEFAULT_VHOST: "/account.ely.by/tests"