From c0aa78d1561fbd24b7cba7b270f5f643af86d2aa Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 8 Jul 2018 18:20:19 +0300 Subject: [PATCH] Implemented WebHooks delivery queue. Completely removed usage of the RabbitMQ. Queue now based on Redis channels. Worker process now extracted as separate docker container. Base image upgraded to the 1.8.0 version (PHP 7.2.7 and pcntl extension). --- .env-dist | 12 - Dockerfile | 2 +- Dockerfile-dev | 2 +- .../authentication/ConfirmEmailForm.php | 6 +- .../accounts/models/BanAccountForm.php | 25 +- .../accounts/models/ChangeEmailForm.php | 23 +- .../accounts/models/ChangeUsernameForm.php | 32 +- .../accounts/models/PardonAccountForm.php | 20 +- api/modules/session/filters/RateLimiter.php | 12 +- api/modules/session/models/SessionModel.php | 6 +- autocompletion.php | 19 +- common/components/RabbitMQ/Component.php | 177 -------- common/components/RabbitMQ/Helper.php | 26 -- common/components/Redis/Cache.php | 13 - common/components/Redis/Connection.php | 415 ----------------- .../components/Redis/ConnectionInterface.php | 19 - common/components/Redis/Key.php | 20 +- common/components/Redis/Set.php | 13 +- common/config/config.php | 23 +- common/models/Account.php | 21 + common/tasks/ClearAccountSessions.php | 64 +++ common/tasks/CreateWebHooksDeliveries.php | 76 ++++ common/tasks/DeliveryWebHook.php | 110 +++++ common/tasks/PullMojangUsername.php | 72 +++ composer.json | 13 +- composer.lock | 426 +++--------------- .../controllers/AccountQueueController.php | 98 ---- console/controllers/AmqpController.php | 72 --- docker-compose.dev.yml | 23 +- docker-compose.prod.yml | 18 +- docker/phpmyadmin/Dockerfile | 2 +- .../supervisor/.gitkeep | 0 docker/supervisor/account-queue-worker.conf | 6 - docker/supervisor/worker-queue.conf | 6 - tests/codeception/api/functional.suite.yml | 1 - tests/codeception/api/unit.suite.yml | 1 - .../authentication/ConfirmEmailFormTest.php | 6 - .../accounts/models/ChangeEmailFormTest.php | 13 - .../models/ChangeUsernameFormTest.php | 23 +- .../modules/internal/models/BanFormTest.php | 27 +- .../internal/models/PardonFormTest.php | 11 - .../common/_support/amqp/Helper.php | 91 ---- .../common/_support/amqp/TestComponent.php | 58 --- .../_support/queue/CodeceptionQueueHelper.php | 7 +- .../common/fixtures/WebHooksEventsFixture.php | 19 + .../common/fixtures/WebHooksFixture.php | 15 + .../common/fixtures/data/webhooks-events.php | 11 + .../common/fixtures/data/webhooks.php | 21 + tests/codeception/common/unit.suite.yml | 1 + .../common/unit/models/AccountTest.php | 39 ++ .../unit/tasks/ClearAccountSessionsTest.php | 44 ++ .../tasks/CreateWebHooksDeliveriesTest.php | 91 ++++ .../common/unit/tasks/DeliveryWebHookTest.php | 132 ++++++ .../unit/tasks/PullMojangUsernameTest.php} | 131 +++--- tests/codeception/config/config.php | 3 - 55 files changed, 933 insertions(+), 1684 deletions(-) delete mode 100644 common/components/RabbitMQ/Component.php delete mode 100644 common/components/RabbitMQ/Helper.php delete mode 100644 common/components/Redis/Cache.php delete mode 100644 common/components/Redis/Connection.php delete mode 100644 common/components/Redis/ConnectionInterface.php create mode 100644 common/tasks/ClearAccountSessions.php create mode 100644 common/tasks/CreateWebHooksDeliveries.php create mode 100644 common/tasks/DeliveryWebHook.php create mode 100644 common/tasks/PullMojangUsername.php delete mode 100644 console/controllers/AccountQueueController.php delete mode 100644 console/controllers/AmqpController.php rename api/models/profile/TwoFactorAuthForm.php => docker/supervisor/.gitkeep (100%) delete mode 100644 docker/supervisor/account-queue-worker.conf delete mode 100644 docker/supervisor/worker-queue.conf delete mode 100644 tests/codeception/common/_support/amqp/Helper.php delete mode 100644 tests/codeception/common/_support/amqp/TestComponent.php create mode 100644 tests/codeception/common/fixtures/WebHooksEventsFixture.php create mode 100644 tests/codeception/common/fixtures/WebHooksFixture.php create mode 100644 tests/codeception/common/fixtures/data/webhooks-events.php create mode 100644 tests/codeception/common/fixtures/data/webhooks.php create mode 100644 tests/codeception/common/unit/tasks/ClearAccountSessionsTest.php create mode 100644 tests/codeception/common/unit/tasks/CreateWebHooksDeliveriesTest.php create mode 100644 tests/codeception/common/unit/tasks/DeliveryWebHookTest.php rename tests/codeception/{console/unit/controllers/AccountQueueControllerTest.php => common/unit/tasks/PullMojangUsernameTest.php} (50%) diff --git a/.env-dist b/.env-dist index a4fcd1c..8357fea 100644 --- a/.env-dist +++ b/.env-dist @@ -29,13 +29,6 @@ REDIS_PORT=6379 REDIS_DATABASE=0 REDIS_PASSWORD= -## Параметры подключения к rabbitmq -RABBITMQ_HOST=rabbitmq -RABBITMQ_PORT=5672 -RABBITMQ_USER=ely-accounts-app -RABBITMQ_PASS=ely-accounts-app-password -RABBITMQ_VHOST=/ely.by - ## Параметры Statsd STATSD_HOST=statsd.ely.by STATSD_PORT=8125 @@ -59,8 +52,3 @@ MYSQL_ROOT_PASSWORD= MYSQL_DATABASE=ely_accounts MYSQL_USER=ely_accounts_user MYSQL_PASSWORD=ely_accounts_password - -# RabbitMQ -RABBITMQ_DEFAULT_USER=ely-accounts-app -RABBITMQ_DEFAULT_PASS=ely-accounts-app-password -RABBITMQ_DEFAULT_VHOST=/ely.by diff --git a/Dockerfile b/Dockerfile index a6f7285..54d8000 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.ely.by/elyby/accounts-php:1.7.0 +FROM registry.ely.by/elyby/accounts-php:1.8.0 # bootstrap скрипт для проекта COPY docker/php/bootstrap.sh /bootstrap.sh diff --git a/Dockerfile-dev b/Dockerfile-dev index 07ec96a..dec0312 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM registry.ely.by/elyby/accounts-php:1.7.0-dev +FROM registry.ely.by/elyby/accounts-php:1.8.0-dev # bootstrap скрипт для проекта COPY docker/php/bootstrap.sh /bootstrap.sh diff --git a/api/models/authentication/ConfirmEmailForm.php b/api/models/authentication/ConfirmEmailForm.php index c813ac0..5d6b7cc 100644 --- a/api/models/authentication/ConfirmEmailForm.php +++ b/api/models/authentication/ConfirmEmailForm.php @@ -1,9 +1,10 @@ createEventTask($account->id, $account->username, null); - $transaction->commit(); return Yii::$app->user->createJwtAuthenticationToken($account, true); diff --git a/api/modules/accounts/models/BanAccountForm.php b/api/modules/accounts/models/BanAccountForm.php index 35f0d30..d35fec4 100644 --- a/api/modules/accounts/models/BanAccountForm.php +++ b/api/modules/accounts/models/BanAccountForm.php @@ -1,13 +1,11 @@ getAccount()->status === Account::STATUS_BANNED) { $this->addError('account', E::ACCOUNT_ALREADY_BANNED); } @@ -54,27 +52,14 @@ class BanAccountForm extends AccountActionForm { $account = $this->getAccount(); $account->status = Account::STATUS_BANNED; if (!$account->save()) { - throw new ErrorException('Cannot ban account'); + throw new ThisShouldNotHappenException('Cannot ban account'); } - $this->createTask(); + Yii::$app->queue->push(ClearAccountSessions::createFromAccount($account)); $transaction->commit(); return true; } - public function createTask(): void { - $model = new AccountBanned(); - $model->accountId = $this->getAccount()->id; - $model->duration = $this->duration; - $model->message = $this->message; - - $message = Amqp::getInstance()->prepareMessage($model, [ - 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, - ]); - - Amqp::sendToEventsExchange('accounts.account-banned', $message); - } - } diff --git a/api/modules/accounts/models/ChangeEmailForm.php b/api/modules/accounts/models/ChangeEmailForm.php index 1f03b67..3de16c6 100644 --- a/api/modules/accounts/models/ChangeEmailForm.php +++ b/api/modules/accounts/models/ChangeEmailForm.php @@ -2,13 +2,10 @@ namespace api\modules\accounts\models; use api\aop\annotations\CollectModelMetrics; +use api\exceptions\ThisShouldNotHappenException; use api\validators\EmailActivationKeyValidator; -use common\helpers\Amqp; -use common\models\amqp\EmailChanged; use common\models\EmailActivation; -use PhpAmqpLib\Message\AMQPMessage; use Yii; -use yii\base\ErrorException; class ChangeEmailForm extends AccountActionForm { @@ -35,30 +32,14 @@ class ChangeEmailForm extends AccountActionForm { $activation->delete(); $account = $this->getAccount(); - $oldEmail = $account->email; $account->email = $activation->newEmail; if (!$account->save()) { - throw new ErrorException('Cannot save new account email value'); + throw new ThisShouldNotHappenException('Cannot save new account email value'); } - $this->createTask($account->id, $account->email, $oldEmail); - $transaction->commit(); return true; } - public function createTask(int $accountId, string $newEmail, string $oldEmail): void { - $model = new EmailChanged(); - $model->accountId = $accountId; - $model->oldEmail = $oldEmail; - $model->newEmail = $newEmail; - - $message = Amqp::getInstance()->prepareMessage($model, [ - 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, - ]); - - Amqp::sendToEventsExchange('accounts.email-changed', $message); - } - } diff --git a/api/modules/accounts/models/ChangeUsernameForm.php b/api/modules/accounts/models/ChangeUsernameForm.php index 86fa961..53ad10d 100644 --- a/api/modules/accounts/models/ChangeUsernameForm.php +++ b/api/modules/accounts/models/ChangeUsernameForm.php @@ -4,13 +4,10 @@ namespace api\modules\accounts\models; use api\aop\annotations\CollectModelMetrics; use api\exceptions\ThisShouldNotHappenException; use api\validators\PasswordRequiredValidator; -use common\helpers\Amqp; -use common\models\amqp\UsernameChanged; use common\models\UsernameHistory; +use common\tasks\PullMojangUsername; use common\validators\UsernameValidator; -use PhpAmqpLib\Message\AMQPMessage; use Yii; -use yii\base\ErrorException; class ChangeUsernameForm extends AccountActionForm { @@ -42,7 +39,6 @@ class ChangeUsernameForm extends AccountActionForm { $transaction = Yii::$app->db->beginTransaction(); - $oldNickname = $account->username; $account->username = $this->username; if (!$account->save()) { throw new ThisShouldNotHappenException('Cannot save account model with new username'); @@ -52,36 +48,14 @@ class ChangeUsernameForm extends AccountActionForm { $usernamesHistory->account_id = $account->id; $usernamesHistory->username = $account->username; if (!$usernamesHistory->save()) { - throw new ErrorException('Cannot save username history record'); + throw new ThisShouldNotHappenException('Cannot save username history record'); } - $this->createEventTask($account->id, $account->username, $oldNickname); + Yii::$app->queue->push(PullMojangUsername::createFromAccount($account)); $transaction->commit(); return true; } - /** - * TODO: вынести это в отдельную сущность, т.к. эта команда используется внутри формы регистрации - * - * @param integer $accountId - * @param string $newNickname - * @param string $oldNickname - * - * @throws \PhpAmqpLib\Exception\AMQPExceptionInterface|\yii\base\Exception - */ - public function createEventTask($accountId, $newNickname, $oldNickname): void { - $model = new UsernameChanged(); - $model->accountId = $accountId; - $model->oldUsername = $oldNickname; - $model->newUsername = $newNickname; - - $message = Amqp::getInstance()->prepareMessage($model, [ - 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, - ]); - - Amqp::sendToEventsExchange('accounts.username-changed', $message); - } - } diff --git a/api/modules/accounts/models/PardonAccountForm.php b/api/modules/accounts/models/PardonAccountForm.php index 2c337d7..fe9c073 100644 --- a/api/modules/accounts/models/PardonAccountForm.php +++ b/api/modules/accounts/models/PardonAccountForm.php @@ -1,13 +1,10 @@ getAccount(); $account->status = Account::STATUS_ACTIVE; if (!$account->save()) { - throw new ErrorException('Cannot pardon account'); + throw new ThisShouldNotHappenException('Cannot pardon account'); } - $this->createTask(); - $transaction->commit(); return true; } - public function createTask(): void { - $model = new AccountPardoned(); - $model->accountId = $this->getAccount()->id; - - $message = Amqp::getInstance()->prepareMessage($model, [ - 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, - ]); - - Amqp::sendToEventsExchange('accounts.account-pardoned', $message); - } - } diff --git a/api/modules/session/filters/RateLimiter.php b/api/modules/session/filters/RateLimiter.php index 72081bc..96be790 100644 --- a/api/modules/session/filters/RateLimiter.php +++ b/api/modules/session/filters/RateLimiter.php @@ -56,10 +56,9 @@ class RateLimiter extends \yii\filters\RateLimiter { $ip = $request->getUserIP(); $key = $this->buildKey($ip); - $redis = $this->getRedis(); - $countRequests = (int)$redis->incr($key); + $countRequests = (int)Yii::$app->redis->incr($key); if ($countRequests === 1) { - $redis->executeCommand('EXPIRE', [$key, $this->limitTime]); + Yii::$app->redis->expire($key, $this->limitTime); } if ($countRequests > $this->limit) { @@ -67,13 +66,6 @@ class RateLimiter extends \yii\filters\RateLimiter { } } - /** - * @return \common\components\Redis\Connection - */ - public function getRedis() { - return Yii::$app->redis; - } - /** * @param Request $request * @return OauthClient|null diff --git a/api/modules/session/models/SessionModel.php b/api/modules/session/models/SessionModel.php index ed5e626..7d37728 100644 --- a/api/modules/session/models/SessionModel.php +++ b/api/modules/session/models/SessionModel.php @@ -19,7 +19,7 @@ class SessionModel { public static function find(string $username, string $serverId): ?self { $key = static::buildKey($username, $serverId); - $result = Yii::$app->redis->executeCommand('GET', [$key]); + $result = Yii::$app->redis->get($key); if (!$result) { return null; } @@ -36,11 +36,11 @@ class SessionModel { 'serverId' => $this->serverId, ]); - return Yii::$app->redis->executeCommand('SETEX', [$key, self::KEY_TIME, $data]); + return Yii::$app->redis->setex($key, self::KEY_TIME, $data); } public function delete() { - return Yii::$app->redis->executeCommand('DEL', [static::buildKey($this->username, $this->serverId)]); + return Yii::$app->redis->del(static::buildKey($this->username, $this->serverId)); } public function getAccount(): ?Account { diff --git a/autocompletion.php b/autocompletion.php index 07b6667..a70d827 100644 --- a/autocompletion.php +++ b/autocompletion.php @@ -16,16 +16,15 @@ class Yii extends \yii\BaseYii { * Class BaseApplication * Used for properties that are identical for both WebApplication and ConsoleApplication * - * @property \yii\db\Connection $unbufferedDb - * @property \yii\swiftmailer\Mailer $mailer - * @property \common\components\Redis\Connection $redis - * @property \common\components\RabbitMQ\Component $amqp - * @property \GuzzleHttp\Client $guzzle - * @property \common\components\EmailRenderer $emailRenderer - * @property \mito\sentry\Component $sentry - * @property \api\components\OAuth2\Component $oauth - * @property \common\components\StatsD $statsd - * @property \yii\queue\Queue $queue + * @property \yii\db\Connection $unbufferedDb + * @property \yii\swiftmailer\Mailer $mailer + * @property \yii\redis\Connection $redis + * @property \GuzzleHttp\Client $guzzle + * @property \common\components\EmailRenderer $emailRenderer + * @property \mito\sentry\Component $sentry + * @property \api\components\OAuth2\Component $oauth + * @property \common\components\StatsD $statsd + * @property \yii\queue\Queue $queue */ abstract class BaseApplication extends yii\base\Application { } diff --git a/common/components/RabbitMQ/Component.php b/common/components/RabbitMQ/Component.php deleted file mode 100644 index b7e6177..0000000 --- a/common/components/RabbitMQ/Component.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - * @property AMQPStreamConnection $connection AMQP connection. - * @property AMQPChannel $channel AMQP channel. - */ -class Component extends \yii\base\Component { - - public const TYPE_TOPIC = 'topic'; - public const TYPE_DIRECT = 'direct'; - public const TYPE_HEADERS = 'headers'; - public const TYPE_FANOUT = 'fanout'; - - /** - * @var string - */ - public $host = '127.0.0.1'; - - /** - * @var integer - */ - public $port = 5672; - - /** - * @var string - */ - public $user; - - /** - * @var string - */ - public $password; - - /** - * @var string - */ - public $vhost = '/'; - - /** - * @var AMQPStreamConnection - */ - protected $amqpConnection; - - /** - * @var AMQPChannel[] - */ - protected $channels = []; - - /** - * @inheritdoc - */ - public function init() { - parent::init(); - if (empty($this->user)) { - throw new Exception("Parameter 'user' was not set for AMQP connection."); - } - } - - /** - * @return AMQPStreamConnection - */ - public function getConnection() { - if (!$this->amqpConnection) { - $this->amqpConnection = new AMQPStreamConnection( - $this->host, - $this->port, - $this->user, - $this->password, - $this->vhost - ); - } - - return $this->amqpConnection; - } - - /** - * @param string $channel_id - * @return AMQPChannel - */ - public function getChannel($channel_id = null) { - $index = $channel_id ?: 'default'; - if (!array_key_exists($index, $this->channels)) { - $this->channels[$index] = $this->getConnection()->channel($channel_id); - } - - return $this->channels[$index]; - } - - // TODO: метод sendToQueue - - /** - * Sends message to the exchange. - * - * @param string $exchangeName - * @param string $routingKey - * @param string|array $message - * @param array $exchangeArgs - * @param array $publishArgs - */ - public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) { - $message = $this->prepareMessage($message); - $channel = $this->getChannel(); - $channel->exchange_declare(...$this->prepareExchangeArgs($exchangeName, $exchangeArgs)); - $channel->basic_publish(...$this->preparePublishArgs($message, $exchangeName, $routingKey, $publishArgs)); - } - - /** - * Returns prepaired AMQP message. - * - * @param string|array|object $message - * @param array $properties - * @return AMQPMessage - * @throws Exception If message is empty. - */ - public function prepareMessage($message, $properties = null) { - if ($message instanceof AMQPMessage) { - return $message; - } - - if (empty($message)) { - throw new Exception('AMQP message can not be empty'); - } - - if (is_array($message) || is_object($message)) { - $message = Json::encode($message); - } - - return new AMQPMessage($message, $properties); - } - - /** - * Объединяет переданный набор аргументов с поведением по умолчанию - * - * @param string $exchangeName - * @param array $args - * @return array - */ - protected function prepareExchangeArgs($exchangeName, array $args) { - return array_replace([ - $exchangeName, - self::TYPE_FANOUT, - false, - false, - false, - ], $args); - } - - /** - * Объединяет переданный набор аргументов с поведением по умолчанию - * - * @param AMQPMessage $message - * @param string $exchangeName - * @param string $routeKey - * @param array $args - * - * @return array - */ - protected function preparePublishArgs($message, $exchangeName, $routeKey, array $args) { - return array_replace([ - $message, - $exchangeName, - $routeKey, - ], $args); - } - -} diff --git a/common/components/RabbitMQ/Helper.php b/common/components/RabbitMQ/Helper.php deleted file mode 100644 index e3b5f7a..0000000 --- a/common/components/RabbitMQ/Helper.php +++ /dev/null @@ -1,26 +0,0 @@ -amqp; - } - - public static function sendToExchange($exchange, $routingKey, $message, $exchangeArgs = []) { - static::getInstance()->sendToExchange($exchange, $routingKey, $message, $exchangeArgs); - } - - public static function sendToEventsExchange($routingKey, $message) { - static::sendToExchange('events', $routingKey, $message, [ - 1 => Component::TYPE_TOPIC, // type -> topic - 3 => true, // durable -> true - ]); - } - -} diff --git a/common/components/Redis/Cache.php b/common/components/Redis/Cache.php deleted file mode 100644 index 6a120a1..0000000 --- a/common/components/Redis/Cache.php +++ /dev/null @@ -1,13 +0,0 @@ -redis = Instance::ensure($this->redis, ConnectionInterface::class); - } - -} diff --git a/common/components/Redis/Connection.php b/common/components/Redis/Connection.php deleted file mode 100644 index 28d62c4..0000000 --- a/common/components/Redis/Connection.php +++ /dev/null @@ -1,415 +0,0 @@ -executeCommand($name, $params); - } - - return parent::__call($name, $params); - } - - public function getConnection(): ClientInterface { - if ($this->_client === null) { - $this->_client = new Client($this->prepareParams(), $this->options); - } - - return $this->_client; - } - - public function executeCommand(string $name, array $params = []) { - return $this->getConnection()->$name(...$params); - } - - private function prepareParams() { - if ($this->parameters !== null) { - return $this->parameters; - } - - if ($this->unixSocket) { - $parameters = [ - 'scheme' => 'unix', - 'path' => $this->unixSocket, - ]; - } else { - $parameters = [ - 'scheme' => 'tcp', - 'host' => $this->hostname, - 'port' => $this->port, - ]; - } - - return array_merge($parameters, [ - 'database' => $this->database, - ]); - } - -} diff --git a/common/components/Redis/ConnectionInterface.php b/common/components/Redis/ConnectionInterface.php deleted file mode 100644 index f4195fe..0000000 --- a/common/components/Redis/ConnectionInterface.php +++ /dev/null @@ -1,19 +0,0 @@ -key = $this->buildKey($key); } - public function getRedis(): Connection { - return Yii::$app->redis; - } - public function getKey(): string { return $this->key; } public function getValue() { - return $this->getRedis()->get($this->key); + return Yii::$app->redis->get($this->key); } public function setValue($value): self { - $this->getRedis()->set($this->key, $value); - + Yii::$app->redis->set($this->key, $value); return $this; } public function delete(): self { - $this->getRedis()->del([$this->getKey()]); - + Yii::$app->redis->del($this->getKey()); return $this; } public function exists(): bool { - return (bool)$this->getRedis()->exists($this->key); + return (bool)Yii::$app->redis->exists($this->key); } public function expire(int $ttl): self { - $this->getRedis()->expire($this->key, $ttl); - + Yii::$app->redis->expire($this->key, $ttl); return $this; } public function expireAt(int $unixTimestamp): self { - $this->getRedis()->expireat($this->key, $unixTimestamp); - + Yii::$app->redis->expireat($this->key, $unixTimestamp); return $this; } diff --git a/common/components/Redis/Set.php b/common/components/Redis/Set.php index b106304..b6a07ec 100644 --- a/common/components/Redis/Set.php +++ b/common/components/Redis/Set.php @@ -3,23 +3,22 @@ namespace common\components\Redis; use ArrayIterator; use IteratorAggregate; +use Yii; class Set extends Key implements IteratorAggregate { public function add($value): self { - $this->getRedis()->sadd($this->getKey(), $value); - + Yii::$app->redis->sadd($this->getKey(), $value); return $this; } public function remove($value): self { - $this->getRedis()->srem($this->getKey(), $value); - + Yii::$app->redis->srem($this->getKey(), $value); return $this; } public function members(): array { - return $this->getRedis()->smembers($this->getKey()); + return Yii::$app->redis->smembers($this->getKey()); } public function getValue(): array { @@ -31,11 +30,11 @@ class Set extends Key implements IteratorAggregate { return parent::exists(); } - return (bool)$this->getRedis()->sismember($this->getKey(), $value); + return (bool)Yii::$app->redis->sismember($this->getKey(), $value); } public function diff(array $sets): array { - return $this->getRedis()->sdiff([$this->getKey(), implode(' ', $sets)]); + return Yii::$app->redis->sdiff([$this->getKey(), implode(' ', $sets)]); } /** diff --git a/common/config/config.php b/common/config/config.php index 36b1266..cd71980 100644 --- a/common/config/config.php +++ b/common/config/config.php @@ -4,8 +4,7 @@ return [ 'vendorPath' => dirname(__DIR__, 2) . '/vendor', 'components' => [ 'cache' => [ - 'class' => common\components\Redis\Cache::class, - 'redis' => 'redis', + 'class' => yii\redis\Cache::class, ], 'db' => [ 'class' => yii\db\Connection::class, @@ -61,20 +60,12 @@ return [ 'passwordHashStrategy' => 'password_hash', ], 'redis' => [ - 'class' => common\components\Redis\Connection::class, + 'class' => yii\redis\Connection::class, 'hostname' => getenv('REDIS_HOST') ?: 'redis', 'password' => getenv('REDIS_PASS') ?: null, 'port' => getenv('REDIS_PORT') ?: 6379, 'database' => getenv('REDIS_DATABASE') ?: 0, ], - 'amqp' => [ - 'class' => common\components\RabbitMQ\Component::class, - 'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq', - 'port' => getenv('RABBITMQ_PORT') ?: 5672, - 'user' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASS'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], 'guzzle' => [ 'class' => GuzzleHttp\Client::class, ], @@ -97,15 +88,7 @@ return [ 'namespace' => getenv('STATSD_NAMESPACE') ?: 'ely.accounts.' . gethostname() . '.app', ], 'queue' => [ - 'class' => yii\queue\amqp_interop\Queue::class, - 'driver' => yii\queue\amqp_interop\Queue::ENQUEUE_AMQP_LIB, - 'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq', - 'port' => getenv('RABBITMQ_PORT') ?: 5672, - 'user' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASS'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'queueName' => 'worker', - 'exchangeName' => 'tasks', + 'class' => yii\queue\redis\Queue::class, ], ], 'container' => [ diff --git a/common/models/Account.php b/common/models/Account.php index 06d1961..dfc8dfc 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -1,7 +1,10 @@ registration_ip === null ? null : inet_ntop($this->registration_ip); } + public function afterSave($insert, $changedAttributes) { + parent::afterSave($insert, $changedAttributes); + + if ($insert) { + return; + } + + $meaningfulFields = ['username', 'email', 'uuid', 'status', 'lang']; + $meaningfulChangedAttributes = array_filter($changedAttributes, function(string $key) use ($meaningfulFields) { + return in_array($key, $meaningfulFields, true); + }, ARRAY_FILTER_USE_KEY); + if (empty($meaningfulChangedAttributes)) { + return; + } + + Yii::$app->queue->push(CreateWebHooksDeliveries::createAccountEdit($this, $meaningfulChangedAttributes)); + } + } diff --git a/common/tasks/ClearAccountSessions.php b/common/tasks/ClearAccountSessions.php new file mode 100644 index 0000000..86a5043 --- /dev/null +++ b/common/tasks/ClearAccountSessions.php @@ -0,0 +1,64 @@ +accountId = $account->id; + + return $result; + } + + /** + * @return int time to reserve in seconds + */ + public function getTtr(): int { + return 5 * 60; + } + + /** + * @param int $attempt number + * @param \Exception|\Throwable $error from last execute of the job + * + * @return bool + */ + public function canRetry($attempt, $error): bool { + return true; + } + + /** + * @param \yii\queue\Queue $queue which pushed and is handling the job + * @throws \Exception + */ + public function execute($queue): void { + $account = Account::findOne($this->accountId); + if ($account === null) { + return; + } + + foreach ($account->getSessions()->each(100, Yii::$app->unbufferedDb) as $authSession) { + /** @var \common\models\AccountSession $authSession */ + $authSession->delete(); + } + + foreach ($account->getMinecraftAccessKeys()->each(100, Yii::$app->unbufferedDb) as $key) { + /** @var \common\models\MinecraftAccessKey $key */ + $key->delete(); + } + + foreach ($account->getOauthSessions()->each(100, Yii::$app->unbufferedDb) as $oauthSession) { + /** @var \common\models\OauthSession $oauthSession */ + $oauthSession->delete(); + } + } + +} diff --git a/common/tasks/CreateWebHooksDeliveries.php b/common/tasks/CreateWebHooksDeliveries.php new file mode 100644 index 0000000..8ba6fef --- /dev/null +++ b/common/tasks/CreateWebHooksDeliveries.php @@ -0,0 +1,76 @@ +type = 'account.edit'; + $result->payloads = [ + 'id' => $account->id, + 'uuid' => $account->uuid, + 'username' => $account->username, + 'email' => $account->email, + 'lang' => $account->lang, + 'isActive' => $account->status === Account::STATUS_ACTIVE, + 'registered' => date('c', (int)$account->created_at), + 'changedAttributes' => $changedAttributes, + ]; + + return $result; + } + + /** + * @return int time to reserve in seconds + */ + public function getTtr() { + return 10; + } + + /** + * @param int $attempt number + * @param \Exception|\Throwable $error from last execute of the job + * + * @return bool + */ + public function canRetry($attempt, $error) { + return true; + } + + /** + * @param \yii\queue\Queue $queue which pushed and is handling the job + */ + public function execute($queue) { + /** @var WebHook[] $targets */ + $targets = WebHook::find() + ->joinWith('events e', false) + ->andWhere(['e.event_type' => $this->type]) + ->all(); + foreach ($targets as $target) { + $job = new DeliveryWebHook(); + $job->type = $this->type; + $job->url = $target->url; + $job->secret = $target->secret; + $job->payloads = $this->payloads; + Yii::$app->queue->push($job); + } + } + +} diff --git a/common/tasks/DeliveryWebHook.php b/common/tasks/DeliveryWebHook.php new file mode 100644 index 0000000..7561d2a --- /dev/null +++ b/common/tasks/DeliveryWebHook.php @@ -0,0 +1,110 @@ += 5) { + return false; + } + + if ($error instanceof ServerException || $error instanceof ConnectException) { + return true; + } + + return false; + } + + /** + * @param \yii\queue\Queue $queue which pushed and is handling the job + * + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function execute($queue): void { + $client = $this->createClient(); + try { + $client->request('POST', $this->url, [ + 'headers' => [ + 'User-Agent' => 'Account-Ely-Hookshot/' . Yii::$app->version, + 'X-Ely-Accounts-Event' => $this->type, + ], + 'form_params' => $this->payloads, + ]); + } catch (ClientException $e) { + Yii::info("Delivery for {$this->url} has failed with {$e->getResponse()->getStatusCode()} status."); + return; + } + } + + protected function createClient(): ClientInterface { + return new GuzzleClient([ + 'handler' => $this->createStack(), + 'timeout' => 60, + 'connect_timeout' => 10, + ]); + } + + protected function createStack(): HandlerStack { + $stack = HandlerStack::create(); + $stack->push(Middleware::mapRequest(function(RequestInterface $request): RequestInterface { + if (empty($this->secret)) { + return $request; + } + + $payload = (string)$request->getBody(); + $signature = hash_hmac('sha1', $payload, $this->secret); + + /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ + return $request->withHeader('X-Hub-Signature', 'sha1=' . $signature); + })); + + return $stack; + } + +} diff --git a/common/tasks/PullMojangUsername.php b/common/tasks/PullMojangUsername.php new file mode 100644 index 0000000..169ddfb --- /dev/null +++ b/common/tasks/PullMojangUsername.php @@ -0,0 +1,72 @@ +username = $account->username; + + return $result; + } + + /** + * @param \yii\queue\Queue $queue which pushed and is handling the job + * + * @throws \Exception + */ + public function execute($queue) { + Yii::$app->statsd->inc('queue.pullMojangUsername.attempt'); + $mojangApi = $this->createMojangApi(); + try { + $response = $mojangApi->usernameToUUID($this->username); + Yii::$app->statsd->inc('queue.pullMojangUsername.found'); + } catch (NoContentException $e) { + $response = false; + Yii::$app->statsd->inc('queue.pullMojangUsername.not_found'); + } catch (RequestException | MojangApiException $e) { + Yii::$app->statsd->inc('queue.pullMojangUsername.error'); + return; + } + + /** @var MojangUsername|null $mojangUsername */ + $mojangUsername = MojangUsername::findOne($this->username); + if ($response === false) { + if ($mojangUsername !== null) { + $mojangUsername->delete(); + } + } else { + if ($mojangUsername === null) { + $mojangUsername = new MojangUsername(); + $mojangUsername->username = $response->name; + $mojangUsername->uuid = $response->id; + } else { + $mojangUsername->uuid = $response->id; + $mojangUsername->touch('last_pulled_at'); + } + + if (!$mojangUsername->save()) { + throw new ThisShouldNotHappenException('Cannot save mojang username'); + } + } + } + + protected function createMojangApi(): MojangApi { + return new MojangApi(); + } + +} diff --git a/composer.json b/composer.json index 0b2d338..8afc205 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,9 @@ "league/oauth2-server": "^4.1", "yiisoft/yii2-redis": "~2.0.0", "guzzlehttp/guzzle": "^6.0.0", - "php-amqplib/php-amqplib": "^2.6.2", "ely/yii2-tempmail-validator": "^2.0", "emarref/jwt": "~1.0.3", - "ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec", "ely/email-renderer": "dev-master#8aa2e71c5b3b8e4a726c3c090b2997030ba29f73", - "predis/predis": "^1.0", "mito/yii2-sentry": "^1.0", "spomky-labs/otphp": "^9.0.2", "bacon/bacon-qr-code": "^1.0", @@ -26,8 +23,7 @@ "webmozart/assert": "^1.2.0", "goaop/framework": "~2.2.0", "domnikl/statsd": "^2.6", - "yiisoft/yii2-queue": "~2.0.2", - "enqueue/amqp-lib": "^0.8.11" + "yiisoft/yii2-queue": "~2.1.0" }, "require-dev": { "yiisoft/yii2-debug": "*", @@ -40,17 +36,14 @@ "mockery/mockery": "^1.0.0", "php-mock/php-mock-mockery": "^1.2.0", "friendsofphp/php-cs-fixer": "^2.11", - "ely/php-code-style": "^0.1.0" + "ely/php-code-style": "^0.1.0", + "predis/predis": "^1.1" }, "repositories": [ { "type": "composer", "url": "https://asset-packagist.org" }, - { - "type": "git", - "url": "git@gitlab.ely.by:elyby/amqp-controller.git" - }, { "type": "git", "url": "git@gitlab.ely.by:elyby/email-renderer.git" diff --git a/composer.lock b/composer.lock index 19ec143..44bd2a0 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "63497214afa6b50f50d3bc80fe9eb269", + "content-hash": "b576f6f9babd8e00a7fa6768d56c37e9", "packages": [ { "name": "bacon/bacon-qr-code", @@ -153,7 +153,7 @@ "version": "v1.3.2", "source": { "type": "git", - "url": "https://github.com/bestiejs/punycode.js.git", + "url": "git@github.com:bestiejs/punycode.js.git", "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" }, "dist": { @@ -610,42 +610,6 @@ ], "time": "2017-11-15T23:40:40+00:00" }, - { - "name": "ely/amqp-controller", - "version": "dev-master", - "source": { - "type": "git", - "url": "git@gitlab.ely.by:elyby/amqp-controller.git", - "reference": "d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec" - }, - "require": { - "php-amqplib/php-amqplib": "^2.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ely\\Amqp\\": "src/" - } - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ely.by team", - "email": "team@ely.by" - }, - { - "name": "ErickSkrauch", - "email": "erickskrauch@ely.by" - } - ], - "homepage": "http://ely.by", - "keywords": [ - "" - ], - "time": "2016-11-15T19:40:20+00:00" - }, { "name": "ely/email-renderer", "version": "dev-master", @@ -779,117 +743,6 @@ "description": "A JWT implementation", "time": "2016-09-05T20:33:06+00:00" }, - { - "name": "enqueue/amqp-lib", - "version": "0.8.21", - "source": { - "type": "git", - "url": "https://github.com/php-enqueue/amqp-lib.git", - "reference": "5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-enqueue/amqp-lib/zipball/5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75", - "reference": "5a0da2f2eccb2ebda4d0b2526e1753c96cf0ef75", - "shasum": "" - }, - "require": { - "enqueue/amqp-tools": "^0.8.5@dev", - "php": ">=5.6", - "php-amqplib/php-amqplib": "^2.7@dev", - "queue-interop/amqp-interop": "^0.7@dev", - "queue-interop/queue-interop": "^0.6@dev" - }, - "require-dev": { - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "enqueue/test": "^0.8@dev", - "phpunit/phpunit": "~5.4.0", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/config": "^2.8|^3|^4", - "symfony/dependency-injection": "^2.8|^3|^4" - }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.8.x-dev" - } - }, - "autoload": { - "psr-4": { - "Enqueue\\AmqpLib\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Message Queue Amqp Transport", - "homepage": "https://enqueue.forma-pro.com/", - "keywords": [ - "AMQP", - "messaging", - "queue" - ], - "time": "2018-02-16T11:05:22+00:00" - }, - { - "name": "enqueue/amqp-tools", - "version": "0.8.14", - "source": { - "type": "git", - "url": "https://github.com/php-enqueue/amqp-tools.git", - "reference": "f375dee4d8609fca565a80df1c0f238bf0fe774f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-enqueue/amqp-tools/zipball/f375dee4d8609fca565a80df1c0f238bf0fe774f", - "reference": "f375dee4d8609fca565a80df1c0f238bf0fe774f", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "queue-interop/amqp-interop": "^0.7@dev", - "queue-interop/queue-interop": "^0.6@dev" - }, - "require-dev": { - "enqueue/null": "^0.8@dev", - "enqueue/test": "^0.8@dev", - "phpunit/phpunit": "~5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.8.x-dev" - } - }, - "autoload": { - "psr-4": { - "Enqueue\\AmqpTools\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Message Queue Amqp Tools", - "homepage": "https://enqueue.forma-pro.com/", - "keywords": [ - "AMQP", - "messaging", - "queue" - ], - "time": "2018-01-10T12:00:35+00:00" - }, { "name": "ezyang/htmlpurifier", "version": "v4.9.3", @@ -1619,127 +1472,6 @@ ], "time": "2017-09-27T21:40:39+00:00" }, - { - "name": "php-amqplib/php-amqplib", - "version": "v2.7.2", - "source": { - "type": "git", - "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/dfd3694a86f1a7394d3693485259d4074a6ec79b", - "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b", - "shasum": "" - }, - "require": { - "ext-bcmath": "*", - "ext-mbstring": "*", - "php": ">=5.3.0" - }, - "replace": { - "videlalvaro/php-amqplib": "self.version" - }, - "require-dev": { - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "^4.8", - "scrutinizer/ocular": "^1.1", - "squizlabs/php_codesniffer": "^2.5" - }, - "suggest": { - "ext-sockets": "Use AMQPSocketConnection" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "PhpAmqpLib\\": "PhpAmqpLib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-2.1-or-later" - ], - "authors": [ - { - "name": "Alvaro Videla", - "role": "Original Maintainer" - }, - { - "name": "John Kelly", - "email": "johnmkelly86@gmail.com", - "role": "Maintainer" - }, - { - "name": "Raúl Araya", - "email": "nubeiro@gmail.com", - "role": "Maintainer" - } - ], - "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", - "homepage": "https://github.com/php-amqplib/php-amqplib/", - "keywords": [ - "message", - "queue", - "rabbitmq" - ], - "time": "2018-02-11T19:28:00+00:00" - }, - { - "name": "predis/predis", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/nrk/predis.git", - "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", - "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "suggest": { - "ext-curl": "Allows access to Webdis when paired with phpiredis", - "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" - }, - "type": "library", - "autoload": { - "psr-4": { - "Predis\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniele Alessandri", - "email": "suppakilla@gmail.com", - "homepage": "http://clorophilla.net" - } - ], - "description": "Flexible and feature-complete Redis client for PHP and HHVM", - "homepage": "http://github.com/nrk/predis", - "keywords": [ - "nosql", - "predis", - "redis" - ], - "time": "2016-06-16T16:22:20+00:00" - }, { "name": "psr/http-message", "version": "1.0.1", @@ -1790,87 +1522,6 @@ ], "time": "2016-08-06T14:39:51+00:00" }, - { - "name": "queue-interop/amqp-interop", - "version": "0.7.2", - "source": { - "type": "git", - "url": "https://github.com/queue-interop/amqp-interop.git", - "reference": "03cfac42483d07ab45d1896a6a2e1d873a216bba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/queue-interop/amqp-interop/zipball/03cfac42483d07ab45d1896a6a2e1d873a216bba", - "reference": "03cfac42483d07ab45d1896a6a2e1d873a216bba", - "shasum": "" - }, - "require": { - "php": ">=5.5", - "queue-interop/queue-interop": "^0.6@dev" - }, - "require-dev": { - "phpunit/phpunit": "~5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.7.x-dev" - } - }, - "autoload": { - "psr-4": { - "Interop\\Amqp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "time": "2018-01-04T09:52:06+00:00" - }, - { - "name": "queue-interop/queue-interop", - "version": "0.6.1", - "source": { - "type": "git", - "url": "https://github.com/queue-interop/queue-interop.git", - "reference": "38579005c0492c0275bbae31170edf30a7e740fa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/queue-interop/queue-interop/zipball/38579005c0492c0275bbae31170edf30a7e740fa", - "reference": "38579005c0492c0275bbae31170edf30a7e740fa", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.6.x-dev" - } - }, - "autoload": { - "psr-4": { - "Interop\\Queue\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Promoting the interoperability of MQs objects. Based on Java JMS", - "homepage": "https://github.com/queue-interop/queue-interop", - "keywords": [ - "MQ", - "jms", - "message queue", - "messaging", - "queue" - ], - "time": "2017-08-10T11:24:15+00:00" - }, { "name": "ramsey/uuid", "version": "3.7.3", @@ -2701,24 +2352,25 @@ }, { "name": "yiisoft/yii2-queue", - "version": "2.0.2", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/yiisoft/yii2-queue.git", - "reference": "8c2b337f7d9ea934c2affdfc21c9fb387d0a0773" + "reference": "d04b4b3c932081200876a351cc6c3502e89e11b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/8c2b337f7d9ea934c2affdfc21c9fb387d0a0773", - "reference": "8c2b337f7d9ea934c2affdfc21c9fb387d0a0773", + "url": "https://api.github.com/repos/yiisoft/yii2-queue/zipball/d04b4b3c932081200876a351cc6c3502e89e11b8", + "reference": "d04b4b3c932081200876a351cc6c3502e89e11b8", "shasum": "" }, "require": { "php": ">=5.5.0", "symfony/process": "*", - "yiisoft/yii2": "~2.0.13" + "yiisoft/yii2": "~2.0.14" }, "require-dev": { + "aws/aws-sdk-php": ">=2.4", "enqueue/amqp-lib": "^0.8", "jeremeamia/superclosure": "*", "pda/pheanstalk": "*", @@ -2729,6 +2381,7 @@ "yiisoft/yii2-redis": "*" }, "suggest": { + "aws/aws-sdk-php": "Need for aws SQS.", "enqueue/amqp-lib": "Need for AMQP interop queue.", "ext-gearman": "Need for Gearman queue.", "ext-pcntl": "Need for process signals.", @@ -2752,7 +2405,8 @@ "yii\\queue\\file\\": "src/drivers/file", "yii\\queue\\gearman\\": "src/drivers/gearman", "yii\\queue\\redis\\": "src/drivers/redis", - "yii\\queue\\sync\\": "src/drivers/sync" + "yii\\queue\\sync\\": "src/drivers/sync", + "yii\\queue\\sqs\\": "src/drivers/sqs" } }, "notification-url": "https://packagist.org/downloads/", @@ -2765,7 +2419,7 @@ "email": "zhuravljov@gmail.com" } ], - "description": "Yii2 Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk and Gearman", + "description": "Yii2 Queue Extension which supported DB, Redis, RabbitMQ, Beanstalk, SQS and Gearman", "keywords": [ "async", "beanstalk", @@ -2775,9 +2429,10 @@ "queue", "rabbitmq", "redis", + "sqs", "yii" ], - "time": "2017-12-26T17:16:14+00:00" + "time": "2018-05-23T21:04:57+00:00" }, { "name": "yiisoft/yii2-redis", @@ -4657,6 +4312,56 @@ ], "time": "2018-01-06T05:45:45+00:00" }, + { + "name": "predis/predis", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2016-06-16T16:22:20+00:00" + }, { "name": "sebastian/code-unit-reverse-lookup", "version": "1.0.1", @@ -6026,7 +5731,6 @@ "minimum-stability": "stable", "stability-flags": { "roave/security-advisories": 20, - "ely/amqp-controller": 20, "ely/email-renderer": 20 }, "prefer-stable": false, diff --git a/console/controllers/AccountQueueController.php b/console/controllers/AccountQueueController.php deleted file mode 100644 index 04ee523..0000000 --- a/console/controllers/AccountQueueController.php +++ /dev/null @@ -1,98 +0,0 @@ -exchange->topic()->durable(); - $configurator->queue->name('accounts-accounts-events')->durable(); - $configurator->bind->routingKey('accounts.username-changed') - ->add()->routingKey('account.account-banned'); - } - - public function getRoutesMap() { - return [ - 'accounts.username-changed' => 'routeUsernameChanged', - 'accounts.account-banned' => 'routeAccountBanned', - ]; - } - - public function routeUsernameChanged(UsernameChanged $body): bool { - Yii::$app->statsd->inc('worker.account.usernameChanged.attempt'); - $mojangApi = $this->createMojangApi(); - try { - $response = $mojangApi->usernameToUUID($body->newUsername); - Yii::$app->statsd->inc('worker.account.usernameChanged.found'); - } catch (NoContentException $e) { - $response = false; - Yii::$app->statsd->inc('worker.account.usernameChanged.not_found'); - } catch (RequestException $e) { - return true; - } - - /** @var MojangUsername|null $mojangUsername */ - $mojangUsername = MojangUsername::findOne($body->newUsername); - if ($response === false) { - if ($mojangUsername !== null) { - $mojangUsername->delete(); - } - } else { - if ($mojangUsername === null) { - $mojangUsername = new MojangUsername(); - $mojangUsername->username = $response->name; - $mojangUsername->uuid = $response->id; - } else { - $mojangUsername->uuid = $response->id; - $mojangUsername->touch('last_pulled_at'); - } - - $mojangUsername->save(); - } - - return true; - } - - public function routeAccountBanned(AccountBanned $body): bool { - $account = Account::findOne($body->accountId); - if ($account === null) { - Yii::warning('Cannot find banned account ' . $body->accountId . '. Skipping.'); - return true; - } - - foreach ($account->sessions as $authSession) { - $authSession->delete(); - } - - foreach ($account->minecraftAccessKeys as $key) { - $key->delete(); - } - - foreach ($account->oauthSessions as $oauthSession) { - $oauthSession->delete(); - } - - return true; - } - - /** - * @return MojangApi - */ - protected function createMojangApi(): MojangApi { - return new MojangApi(); - } - -} diff --git a/console/controllers/AmqpController.php b/console/controllers/AmqpController.php deleted file mode 100644 index 1c6360c..0000000 --- a/console/controllers/AmqpController.php +++ /dev/null @@ -1,72 +0,0 @@ -start(); - } - - public function getRoutesMap() { - return []; - } - - /** - * Переопределяем метод callback, чтобы избержать логгирования в консоль ошибок, - * связанных с обвалом того или иного соединения. Это нормально, PHP рождён умирать, - * а не работать 24/7 в качестве демона. - * - * @param AMQPMessage $msg - * @throws YiiDbException - */ - public function callback(AMQPMessage $msg) { - try { - $this->_callback($msg); - } catch (YiiDbException $e) { - if ($this->reconnected || !$this->isRestorableException($e)) { - throw $e; - } - - $this->reconnected = true; - Yii::$app->db->close(); - Yii::$app->db->open(); - $this->callback($msg); - } - - $this->reconnected = false; - } - - /** - * @inheritdoc - */ - protected function getConnection() { - return Yii::$app->amqp->getConnection(); - } - - /** - * @inheritdoc - */ - protected function buildRouteActionName($route) { - return ArrayHelper::getValue($this->getRoutesMap(), $route, 'route' . Inflector::camelize($route)); - } - - private function isRestorableException(Exception $e): bool { - return strpos($e->getMessage(), 'MySQL server has gone away') !== false - || strcmp($e->getMessage(), 'Error while sending QUERY packet') !== false; - } - -} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f93f1f7..7082ef4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,7 +7,18 @@ services: depends_on: - db - redis - - rabbitmq + volumes: + - ./:/var/www/html/ + env_file: .env + + worker: + build: + dockerfile: Dockerfile-dev + context: . + command: ['php', 'yii', 'queue/listen', '-v'] + depends_on: + - db + - redis volumes: - ./:/var/www/html/ env_file: .env @@ -34,16 +45,6 @@ services: volumes: - ./data/redis:/data - rabbitmq: - image: rabbitmq:3.6-management - env_file: .env - environment: - - VIRTUAL_HOST=rabbitmq.account.ely.by.local - - VIRTUAL_PORT=15672 - networks: - - default - - nginx-proxy - phpmyadmin: build: ./docker/phpmyadmin environment: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 47b6e78..539c878 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,14 +2,24 @@ version: '2' services: app: image: registry.ely.by/elyby/accounts:latest + restart: always + depends_on: + - db + - redis + env_file: .env + + worker: + image: registry.ely.by/elyby/accounts:latest + restart: always + command: ['php', 'yii', 'queue/listen', '-v'] depends_on: - db - redis - - rabbitmq env_file: .env web: image: registry.ely.by/elyby/accounts-nginx:1.0.3 + restart: always volumes_from: - app links: @@ -21,19 +31,17 @@ services: db: build: ./docker/mariadb + restart: always env_file: .env volumes: - ./data/mysql:/var/lib/mysql redis: image: redis:3.0-alpine + restart: always volumes: - ./data/redis:/data - rabbitmq: - image: rabbitmq:3.6 - env_file: .env - networks: nginx-proxy: external: diff --git a/docker/phpmyadmin/Dockerfile b/docker/phpmyadmin/Dockerfile index 62e24d4..6b03cfe 100644 --- a/docker/phpmyadmin/Dockerfile +++ b/docker/phpmyadmin/Dockerfile @@ -1,4 +1,4 @@ -FROM phpmyadmin/phpmyadmin +FROM phpmyadmin/phpmyadmin:4.7.9-1 RUN printf "\n\nrequire('./config.local.php');\n" >> /www/config.inc.php diff --git a/api/models/profile/TwoFactorAuthForm.php b/docker/supervisor/.gitkeep similarity index 100% rename from api/models/profile/TwoFactorAuthForm.php rename to docker/supervisor/.gitkeep diff --git a/docker/supervisor/account-queue-worker.conf b/docker/supervisor/account-queue-worker.conf deleted file mode 100644 index aed1af3..0000000 --- a/docker/supervisor/account-queue-worker.conf +++ /dev/null @@ -1,6 +0,0 @@ -[program:account-queue-worker] -directory=/var/www/html -command=wait-for-it rabbitmq:5672 -- php yii account-queue -autostart=true -autorestart=true -priority=10 diff --git a/docker/supervisor/worker-queue.conf b/docker/supervisor/worker-queue.conf deleted file mode 100644 index 397c104..0000000 --- a/docker/supervisor/worker-queue.conf +++ /dev/null @@ -1,6 +0,0 @@ -[program:queue-worker] -directory=/var/www/html -command=wait-for-it rabbitmq:5672 -- php yii queue/listen -v -autostart=true -autorestart=true -priority=10 diff --git a/tests/codeception/api/functional.suite.yml b/tests/codeception/api/functional.suite.yml index 9a6ac97..570a025 100644 --- a/tests/codeception/api/functional.suite.yml +++ b/tests/codeception/api/functional.suite.yml @@ -4,7 +4,6 @@ modules: - Filesystem - Yii2 - tests\codeception\common\_support\FixtureHelper - - tests\codeception\common\_support\amqp\Helper - tests\codeception\common\_support\Mockery - Redis - Asserts diff --git a/tests/codeception/api/unit.suite.yml b/tests/codeception/api/unit.suite.yml index 7de7236..beea248 100644 --- a/tests/codeception/api/unit.suite.yml +++ b/tests/codeception/api/unit.suite.yml @@ -3,7 +3,6 @@ modules: enabled: - Yii2: part: [orm, email, fixtures] - - tests\codeception\common\_support\amqp\Helper - tests\codeception\common\_support\queue\CodeceptionQueueHelper - tests\codeception\common\_support\Mockery config: diff --git a/tests/codeception/api/unit/models/authentication/ConfirmEmailFormTest.php b/tests/codeception/api/unit/models/authentication/ConfirmEmailFormTest.php index 404b994..c392830 100644 --- a/tests/codeception/api/unit/models/authentication/ConfirmEmailFormTest.php +++ b/tests/codeception/api/unit/models/authentication/ConfirmEmailFormTest.php @@ -28,12 +28,6 @@ class ConfirmEmailFormTest extends TestCase { /** @var Account $account */ $account = Account::findOne($fixture['account_id']); $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) { diff --git a/tests/codeception/api/unit/modules/accounts/models/ChangeEmailFormTest.php b/tests/codeception/api/unit/modules/accounts/models/ChangeEmailFormTest.php index eb7f8f5..abf041d 100644 --- a/tests/codeception/api/unit/modules/accounts/models/ChangeEmailFormTest.php +++ b/tests/codeception/api/unit/modules/accounts/models/ChangeEmailFormTest.php @@ -32,19 +32,6 @@ class ChangeEmailFormTest extends TestCase { /** @noinspection UnserializeExploitsInspection */ $data = unserialize($newEmailConfirmationFixture['_data']); $this->assertEquals($data['newEmail'], $account->email); - $this->tester->canSeeAmqpMessageIsCreated('events'); - } - - public function testCreateTask() { - /** @var Account $account */ - $account = Account::findOne($this->getAccountId()); - $model = new ChangeEmailForm($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() { diff --git a/tests/codeception/api/unit/modules/accounts/models/ChangeUsernameFormTest.php b/tests/codeception/api/unit/modules/accounts/models/ChangeUsernameFormTest.php index 1931272..67d6db5 100644 --- a/tests/codeception/api/unit/modules/accounts/models/ChangeUsernameFormTest.php +++ b/tests/codeception/api/unit/modules/accounts/models/ChangeUsernameFormTest.php @@ -4,6 +4,7 @@ namespace tests\codeception\api\unit\modules\accounts\models; use api\modules\accounts\models\ChangeUsernameForm; use common\models\Account; use common\models\UsernameHistory; +use common\tasks\PullMojangUsername; use tests\codeception\api\unit\TestCase; use tests\codeception\common\fixtures\AccountFixture; use tests\codeception\common\fixtures\UsernameHistoryFixture; @@ -25,7 +26,10 @@ class ChangeUsernameFormTest extends TestCase { $this->assertTrue($model->performAction()); $this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username); $this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname'])); - $this->tester->canSeeAmqpMessageIsCreated('events'); + /** @var PullMojangUsername $job */ + $job = $this->tester->grabLastQueuedJob(); + $this->assertInstanceOf(PullMojangUsername::class, $job); + $this->assertSame($job->username, 'my_new_nickname'); } public function testPerformActionWithTheSameUsername() { @@ -42,7 +46,7 @@ class ChangeUsernameFormTest extends TestCase { 'username' => $username, ['>=', 'applied_in', $callTime], ]), 'no new UsernameHistory record, if we don\'t change username'); - $this->tester->cantSeeAmqpMessageIsCreated('events'); + $this->assertNull($this->tester->grabLastQueuedJob()); } public function testPerformActionWithChangeCase() { @@ -58,17 +62,10 @@ class ChangeUsernameFormTest extends TestCase { UsernameHistory::findOne(['username' => $newUsername]), 'username should change, if we change case of some letters' ); - $this->tester->canSeeAmqpMessageIsCreated('events'); - } - - public function testCreateTask() { - $model = new ChangeUsernameForm($this->getAccount()); - $model->createEventTask(1, 'test1', 'test'); - $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']); + /** @var PullMojangUsername $job */ + $job = $this->tester->grabLastQueuedJob(); + $this->assertInstanceOf(PullMojangUsername::class, $job); + $this->assertSame($job->username, $newUsername); } private function getAccount(): Account { diff --git a/tests/codeception/api/unit/modules/internal/models/BanFormTest.php b/tests/codeception/api/unit/modules/internal/models/BanFormTest.php index 4cd3c02..3894f7f 100644 --- a/tests/codeception/api/unit/modules/internal/models/BanFormTest.php +++ b/tests/codeception/api/unit/modules/internal/models/BanFormTest.php @@ -4,6 +4,7 @@ namespace tests\codeception\api\unit\modules\internal\models; use api\modules\accounts\models\BanAccountForm; use api\modules\internal\helpers\Error as E; use common\models\Account; +use common\tasks\ClearAccountSessions; use tests\codeception\api\unit\TestCase; class BanFormTest extends TestCase { @@ -35,28 +36,10 @@ class BanFormTest extends TestCase { $model = new BanAccountForm($account); $this->assertTrue($model->performAction()); $this->assertEquals(Account::STATUS_BANNED, $account->status); - $this->tester->canSeeAmqpMessageIsCreated('events'); - } - - public function testCreateTask() { - $account = new Account(); - $account->id = 3; - - $model = new BanAccountForm($account); - $model->createTask(); - $message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true); - $this->assertSame(3, $message['accountId']); - $this->assertSame(-1, $message['duration']); - $this->assertSame('', $message['message']); - - $model = new BanAccountForm($account); - $model->duration = 123; - $model->message = 'test'; - $model->createTask(); - $message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true); - $this->assertSame(3, $message['accountId']); - $this->assertSame(123, $message['duration']); - $this->assertSame('test', $message['message']); + /** @var ClearAccountSessions $job */ + $job = $this->tester->grabLastQueuedJob(); + $this->assertInstanceOf(ClearAccountSessions::class, $job); + $this->assertSame($job->accountId, $account->id); } } diff --git a/tests/codeception/api/unit/modules/internal/models/PardonFormTest.php b/tests/codeception/api/unit/modules/internal/models/PardonFormTest.php index 1d6e2a7..362271f 100644 --- a/tests/codeception/api/unit/modules/internal/models/PardonFormTest.php +++ b/tests/codeception/api/unit/modules/internal/models/PardonFormTest.php @@ -36,17 +36,6 @@ class PardonFormTest extends TestCase { $model = new PardonAccountForm($account); $this->assertTrue($model->performAction()); $this->assertEquals(Account::STATUS_ACTIVE, $account->status); - $this->tester->canSeeAmqpMessageIsCreated('events'); - } - - public function testCreateTask() { - $account = new Account(); - $account->id = 3; - - $model = new PardonAccountForm($account); - $model->createTask(); - $message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true); - $this->assertSame(3, $message['accountId']); } } diff --git a/tests/codeception/common/_support/amqp/Helper.php b/tests/codeception/common/_support/amqp/Helper.php deleted file mode 100644 index e623646..0000000 --- a/tests/codeception/common/_support/amqp/Helper.php +++ /dev/null @@ -1,91 +0,0 @@ -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; - } - - $this->assertCount( - $num, - $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; - } - -} diff --git a/tests/codeception/common/_support/amqp/TestComponent.php b/tests/codeception/common/_support/amqp/TestComponent.php deleted file mode 100644 index ed90730..0000000 --- a/tests/codeception/common/_support/amqp/TestComponent.php +++ /dev/null @@ -1,58 +0,0 @@ -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] ?? []; - } - - $messages = []; - foreach ($this->sentMessages as $exchangeGroup) { - foreach ($exchangeGroup as $message) { - $messages[] = $message; - } - } - - return $messages; - } - -} diff --git a/tests/codeception/common/_support/queue/CodeceptionQueueHelper.php b/tests/codeception/common/_support/queue/CodeceptionQueueHelper.php index 8878ea3..326e7f3 100644 --- a/tests/codeception/common/_support/queue/CodeceptionQueueHelper.php +++ b/tests/codeception/common/_support/queue/CodeceptionQueueHelper.php @@ -14,7 +14,12 @@ class CodeceptionQueueHelper extends Module { */ public function grabLastQueuedJob() { $messages = $this->grabQueueJobs(); - return end($messages); + $last = end($messages); + if ($last === false) { + return null; + } + + return $last; } /** diff --git a/tests/codeception/common/fixtures/WebHooksEventsFixture.php b/tests/codeception/common/fixtures/WebHooksEventsFixture.php new file mode 100644 index 0000000..300a7ea --- /dev/null +++ b/tests/codeception/common/fixtures/WebHooksEventsFixture.php @@ -0,0 +1,19 @@ + 1, + 'event_type' => 'account.edit', + ], + [ + 'webhook_id' => 2, + 'event_type' => 'account.edit', + ], +]; diff --git a/tests/codeception/common/fixtures/data/webhooks.php b/tests/codeception/common/fixtures/data/webhooks.php new file mode 100644 index 0000000..238c90a --- /dev/null +++ b/tests/codeception/common/fixtures/data/webhooks.php @@ -0,0 +1,21 @@ + [ + 'id' => 1, + 'url' => 'http://localhost:80/webhooks/ely', + 'secret' => 'my-secret', + 'created_at' => 1531054333, + ], + 'webhook-without-secret' => [ + 'id' => 2, + 'url' => 'http://localhost:81/webhooks/ely', + 'secret' => null, + 'created_at' => 1531054837, + ], + 'webhook-without-events' => [ + 'id' => 3, + 'url' => 'http://localhost:82/webhooks/ely', + 'secret' => null, + 'created_at' => 1531054990, + ], +]; diff --git a/tests/codeception/common/unit.suite.yml b/tests/codeception/common/unit.suite.yml index 11d4bb4..ac95de3 100644 --- a/tests/codeception/common/unit.suite.yml +++ b/tests/codeception/common/unit.suite.yml @@ -3,6 +3,7 @@ modules: enabled: - Yii2: part: [orm, email, fixtures] + - tests\codeception\common\_support\queue\CodeceptionQueueHelper - tests\codeception\common\_support\Mockery config: Yii2: diff --git a/tests/codeception/common/unit/models/AccountTest.php b/tests/codeception/common/unit/models/AccountTest.php index afb5de9..ab9b477 100644 --- a/tests/codeception/common/unit/models/AccountTest.php +++ b/tests/codeception/common/unit/models/AccountTest.php @@ -1,14 +1,20 @@ assertNull($account->getRegistrationIp()); } + public function testAfterSaveInsertEvent() { + $account = new Account(); + $account->afterSave(true, [ + 'username' => 'old-username', + ]); + $this->assertNull($this->tester->grabLastQueuedJob()); + } + + public function testAfterSaveNotMeaningfulAttributes() { + $account = new Account(); + $account->afterSave(false, [ + 'updatedAt' => time(), + ]); + $this->assertNull($this->tester->grabLastQueuedJob()); + } + + public function testAfterSavePushEvent() { + $changedAttributes = [ + 'username' => 'old-username', + 'email' => 'old-email@ely.by', + 'uuid' => 'c3cc0121-fa87-4818-9c0e-4acb7f9a28c5', + 'status' => 10, + 'lang' => 'en', + ]; + + $account = new Account(); + $account->afterSave(false, $changedAttributes); + /** @var CreateWebHooksDeliveries $job */ + $job = $this->tester->grabLastQueuedJob(); + $this->assertInstanceOf(CreateWebHooksDeliveries::class, $job); + $this->assertSame($job->payloads['changedAttributes'], $changedAttributes); + } + } diff --git a/tests/codeception/common/unit/tasks/ClearAccountSessionsTest.php b/tests/codeception/common/unit/tasks/ClearAccountSessionsTest.php new file mode 100644 index 0000000..af0e554 --- /dev/null +++ b/tests/codeception/common/unit/tasks/ClearAccountSessionsTest.php @@ -0,0 +1,44 @@ + fixtures\AccountFixture::class, + 'oauthSessions' => fixtures\OauthSessionFixture::class, + 'minecraftAccessKeys' => fixtures\MinecraftAccessKeyFixture::class, + 'authSessions' => fixtures\AccountSessionFixture::class, + ]; + } + + public function testCreateFromAccount() { + $account = new Account(); + $account->id = 123; + $task = ClearAccountSessions::createFromAccount($account); + $this->assertSame(123, $task->accountId); + } + + public function testExecute() { + /** @var \common\models\Account $bannedAccount */ + $bannedAccount = $this->tester->grabFixture('accounts', 'banned-account'); + $task = new ClearAccountSessions(); + $task->accountId = $bannedAccount->id; + $task->execute(mock(Queue::class)); + $this->assertEmpty($bannedAccount->sessions); + $this->assertEmpty($bannedAccount->minecraftAccessKeys); + $this->assertEmpty($bannedAccount->oauthSessions); + } + +} diff --git a/tests/codeception/common/unit/tasks/CreateWebHooksDeliveriesTest.php b/tests/codeception/common/unit/tasks/CreateWebHooksDeliveriesTest.php new file mode 100644 index 0000000..4596a51 --- /dev/null +++ b/tests/codeception/common/unit/tasks/CreateWebHooksDeliveriesTest.php @@ -0,0 +1,91 @@ + fixtures\WebHooksFixture::class, + 'webhooksEvents' => fixtures\WebHooksEventsFixture::class, + ]; + } + + public function testCreateAccountEdit() { + $account = new Account(); + $account->id = 123; + $account->username = 'mock-username'; + $account->uuid = 'afc8dc7a-4bbf-4d3a-8699-68890088cf84'; + $account->email = 'mock@ely.by'; + $account->lang = 'en'; + $account->status = Account::STATUS_ACTIVE; + $account->created_at = 1531008814; + $changedAttributes = [ + 'username' => 'old-username', + 'uuid' => 'e05d33e9-ff91-4d26-9f5c-8250f802a87a', + 'email' => 'old-email@ely.by', + 'status' => 0, + ]; + $result = CreateWebHooksDeliveries::createAccountEdit($account, $changedAttributes); + $this->assertInstanceOf(CreateWebHooksDeliveries::class, $result); + $this->assertSame('account.edit', $result->type); + $this->assertArraySubset([ + 'id' => 123, + 'uuid' => 'afc8dc7a-4bbf-4d3a-8699-68890088cf84', + 'username' => 'mock-username', + 'email' => 'mock@ely.by', + 'lang' => 'en', + 'isActive' => true, + 'registered' => '2018-07-08T00:13:34+00:00', + 'changedAttributes' => $changedAttributes, + ], $result->payloads); + } + + public function testExecute() { + $task = new CreateWebHooksDeliveries(); + $task->type = 'account.edit'; + $task->payloads = [ + 'id' => 123, + 'uuid' => 'afc8dc7a-4bbf-4d3a-8699-68890088cf84', + 'username' => 'mock-username', + 'email' => 'mock@ely.by', + 'lang' => 'en', + 'isActive' => true, + 'registered' => '2018-07-08T00:13:34+00:00', + 'changedAttributes' => [ + 'username' => 'old-username', + 'uuid' => 'e05d33e9-ff91-4d26-9f5c-8250f802a87a', + 'email' => 'old-email@ely.by', + 'status' => 0, + ], + ]; + $task->execute(mock(Queue::class)); + /** @var DeliveryWebHook[] $tasks */ + $tasks = $this->tester->grabQueueJobs(); + $this->assertCount(2, $tasks); + + $this->assertInstanceOf(DeliveryWebHook::class, $tasks[0]); + $this->assertSame($task->type, $tasks[0]->type); + $this->assertSame($task->payloads, $tasks[0]->payloads); + $this->assertSame('http://localhost:80/webhooks/ely', $tasks[0]->url); + $this->assertSame('my-secret', $tasks[0]->secret); + + $this->assertInstanceOf(DeliveryWebHook::class, $tasks[1]); + $this->assertSame($task->type, $tasks[1]->type); + $this->assertSame($task->payloads, $tasks[1]->payloads); + $this->assertSame('http://localhost:81/webhooks/ely', $tasks[1]->url); + $this->assertNull($tasks[1]->secret); + } + +} diff --git a/tests/codeception/common/unit/tasks/DeliveryWebHookTest.php b/tests/codeception/common/unit/tasks/DeliveryWebHookTest.php new file mode 100644 index 0000000..af74384 --- /dev/null +++ b/tests/codeception/common/unit/tasks/DeliveryWebHookTest.php @@ -0,0 +1,132 @@ +assertFalse($task->canRetry(1, new \Exception())); + $request = new Request('POST', 'http://localhost'); + $this->assertTrue($task->canRetry(4, new ConnectException('', $request))); + $this->assertTrue($task->canRetry(4, new ServerException('', $request))); + $this->assertFalse($task->canRetry(5, new ConnectException('', $request))); + $this->assertFalse($task->canRetry(5, new ServerException('', $request))); + } + + public function testExecuteSuccessDelivery() { + $this->response = new Response(); + $task = $this->createMockedTask(); + $task->type = 'account.edit'; + $task->url = 'http://localhost:81/webhooks/ely'; + $task->payloads = [ + 'key' => 'value', + 'another' => 'value', + ]; + $task->execute(mock(Queue::class)); + /** @var Request $request */ + $request = $this->historyContainer[0]['request']; + $this->assertSame('http://localhost:81/webhooks/ely', (string)$request->getUri()); + $this->assertStringStartsWith('Account-Ely-Hookshot/', $request->getHeaders()['User-Agent'][0]); + $this->assertSame('account.edit', $request->getHeaders()['X-Ely-Accounts-Event'][0]); + $this->assertSame('application/x-www-form-urlencoded', $request->getHeaders()['Content-Type'][0]); + $this->assertArrayNotHasKey('X-Hub-Signature', $request->getHeaders()); + $this->assertEquals('key=value&another=value', (string)$request->getBody()); + } + + public function testExecuteSuccessDeliveryWithSignature() { + $this->response = new Response(); + $task = $this->createMockedTask(); + $task->type = 'account.edit'; + $task->url = 'http://localhost:81/webhooks/ely'; + $task->secret = 'secret'; + $task->payloads = [ + 'key' => 'value', + 'another' => 'value', + ]; + $task->execute(mock(Queue::class)); + /** @var Request $request */ + $request = $this->historyContainer[0]['request']; + $this->assertSame('http://localhost:81/webhooks/ely', (string)$request->getUri()); + $this->assertStringStartsWith('Account-Ely-Hookshot/', $request->getHeaders()['User-Agent'][0]); + $this->assertSame('account.edit', $request->getHeaders()['X-Ely-Accounts-Event'][0]); + $this->assertSame('application/x-www-form-urlencoded', $request->getHeaders()['Content-Type'][0]); + $this->assertSame('sha1=3c0b1eef564b2d3a5e9c0f2a8302b1b42b3d4784', $request->getHeaders()['X-Hub-Signature'][0]); + $this->assertEquals('key=value&another=value', (string)$request->getBody()); + } + + public function testExecuteHandleClientException() { + $this->response = new Response(403); + $task = $this->createMockedTask(); + $task->type = 'account.edit'; + $task->url = 'http://localhost:81/webhooks/ely'; + $task->secret = 'secret'; + $task->payloads = [ + 'key' => 'value', + 'another' => 'value', + ]; + $task->execute(mock(Queue::class)); + } + + /** + * @expectedException \GuzzleHttp\Exception\ServerException + */ + public function testExecuteUnhandledException() { + $this->response = new Response(502); + $task = $this->createMockedTask(); + $task->type = 'account.edit'; + $task->url = 'http://localhost:81/webhooks/ely'; + $task->secret = 'secret'; + $task->payloads = [ + 'key' => 'value', + 'another' => 'value', + ]; + $task->execute(mock(Queue::class)); + } + + private function createMockedTask(): DeliveryWebHook { + $container = &$this->historyContainer; + $response = $this->response; + return new class ($container, $response) extends DeliveryWebHook { + private $historyContainer; + + private $response; + + public function __construct(array &$historyContainer, $response) { + $this->historyContainer = &$historyContainer; + $this->response = $response; + } + + protected function createStack(): HandlerStack { + $stack = parent::createStack(); + $stack->setHandler(new MockHandler([$this->response])); + $stack->push(Middleware::history($this->historyContainer)); + + return $stack; + } + }; + } + +} diff --git a/tests/codeception/console/unit/controllers/AccountQueueControllerTest.php b/tests/codeception/common/unit/tasks/PullMojangUsernameTest.php similarity index 50% rename from tests/codeception/console/unit/controllers/AccountQueueControllerTest.php rename to tests/codeception/common/unit/tasks/PullMojangUsernameTest.php index 0fbe3fe..cac50b4 100644 --- a/tests/codeception/console/unit/controllers/AccountQueueControllerTest.php +++ b/tests/codeception/common/unit/tasks/PullMojangUsernameTest.php @@ -1,30 +1,32 @@ AccountFixture::class, 'mojangUsernames' => MojangUsernameFixture::class, ]; } @@ -32,10 +34,9 @@ class AccountQueueControllerTest extends TestCase { public function _before() { parent::_before(); - /** @var AccountQueueController|\PHPUnit_Framework_MockObject_MockObject $controller */ - $controller = $this->getMockBuilder(AccountQueueController::class) + /** @var PullMojangUsername|\PHPUnit_Framework_MockObject_MockObject $task */ + $task = $this->getMockBuilder(PullMojangUsername::class) ->setMethods(['createMojangApi']) - ->setConstructorArgs(['account-queue', Yii::$app]) ->getMock(); /** @var Api|\PHPUnit_Framework_MockObject_MockObject $apiMock */ @@ -54,30 +55,31 @@ class AccountQueueControllerTest extends TestCase { return $this->expectedResponse; }); - $controller + $task ->expects($this->any()) ->method('createMojangApi') ->willReturn($apiMock); - $this->controller = $controller; + $this->task = $task; } - public function testRouteUsernameChangedUsernameExists() { + public function testCreateFromAccount() { + $account = new Account(); + $account->username = 'find-me'; + $result = PullMojangUsername::createFromAccount($account); + $this->assertSame('find-me', $result->username); + } + + public function testExecuteUsernameExists() { $expectedResponse = new UsernameToUUIDResponse(); $expectedResponse->id = '069a79f444e94726a5befca90e38aaf5'; $expectedResponse->name = 'Notch'; $this->expectedResponse = $expectedResponse; - /** @var \common\models\Account $accountInfo */ - $accountInfo = $this->tester->grabFixture('accounts', 'admin'); - /** @var MojangUsername $mojangUsernameFixture */ + /** @var \common\models\MojangUsername $mojangUsernameFixture */ $mojangUsernameFixture = $this->tester->grabFixture('mojangUsernames', 'Notch'); - $body = new UsernameChanged([ - 'accountId' => $accountInfo->id, - 'oldUsername' => $accountInfo->username, - 'newUsername' => 'Notch', - ]); - $this->controller->routeUsernameChanged($body); + $this->task->username = 'Notch'; + $this->task->execute(mock(Queue::class)); /** @var MojangUsername|null $mojangUsername */ $mojangUsername = MojangUsername::findOne('Notch'); $this->assertInstanceOf(MojangUsername::class, $mojangUsername); @@ -85,81 +87,62 @@ class AccountQueueControllerTest extends TestCase { $this->assertLessThanOrEqual(time(), $mojangUsername->last_pulled_at); } - public function testRouteUsernameChangedUsernameNotExists() { + public function testExecuteChangedUsernameExists() { + $expectedResponse = new UsernameToUUIDResponse(); + $expectedResponse->id = '069a79f444e94726a5befca90e38aaf5'; + $expectedResponse->name = 'Notch'; + $this->expectedResponse = $expectedResponse; + + /** @var MojangUsername $mojangUsernameFixture */ + $mojangUsernameFixture = $this->tester->grabFixture('mojangUsernames', 'Notch'); + $this->task->username = 'Notch'; + $this->task->execute(mock(Queue::class)); + /** @var MojangUsername|null $mojangUsername */ + $mojangUsername = MojangUsername::findOne('Notch'); + $this->assertInstanceOf(MojangUsername::class, $mojangUsername); + $this->assertGreaterThan($mojangUsernameFixture->last_pulled_at, $mojangUsername->last_pulled_at); + $this->assertLessThanOrEqual(time(), $mojangUsername->last_pulled_at); + } + + public function testExecuteChangedUsernameNotExists() { $expectedResponse = new UsernameToUUIDResponse(); $expectedResponse->id = '607153852b8c4909811f507ed8ee737f'; $expectedResponse->name = 'Chest'; $this->expectedResponse = $expectedResponse; - /** @var \common\models\Account $accountInfo */ - $accountInfo = $this->tester->grabFixture('accounts', 'admin'); - $body = new UsernameChanged([ - 'accountId' => $accountInfo['id'], - 'oldUsername' => $accountInfo['username'], - 'newUsername' => 'Chest', - ]); - $this->controller->routeUsernameChanged($body); + $this->task->username = 'Chest'; + $this->task->execute(mock(Queue::class)); /** @var MojangUsername|null $mojangUsername */ $mojangUsername = MojangUsername::findOne('Chest'); $this->assertInstanceOf(MojangUsername::class, $mojangUsername); } - public function testRouteUsernameChangedRemoveIfExistsNoMore() { + public function testExecuteRemoveIfExistsNoMore() { $this->expectedResponse = false; - /** @var \common\models\Account $accountInfo */ - $accountInfo = $this->tester->grabFixture('accounts', 'admin'); $username = $this->tester->grabFixture('mojangUsernames', 'not-exists')['username']; - $body = new UsernameChanged([ - 'accountId' => $accountInfo['id'], - 'oldUsername' => $accountInfo['username'], - 'newUsername' => $username, - ]); - $this->controller->routeUsernameChanged($body); + $this->task->username = $username; + $this->task->execute(mock(Queue::class)); /** @var MojangUsername|null $mojangUsername */ $mojangUsername = MojangUsername::findOne($username); $this->assertNull($mojangUsername); } - public function testRouteUsernameChangedUuidUpdated() { + public function testExecuteUuidUpdated() { $expectedResponse = new UsernameToUUIDResponse(); $expectedResponse->id = 'f498513ce8c84773be26ecfc7ed5185d'; $expectedResponse->name = 'jeb'; $this->expectedResponse = $expectedResponse; - /** @var \common\models\Account $accountInfo */ - $accountInfo = $this->tester->grabFixture('accounts', 'admin'); /** @var MojangUsername $mojangInfo */ $mojangInfo = $this->tester->grabFixture('mojangUsernames', 'uuid-changed'); $username = $mojangInfo['username']; - $body = new UsernameChanged([ - 'accountId' => $accountInfo['id'], - 'oldUsername' => $accountInfo['username'], - 'newUsername' => $username, - ]); - $this->controller->routeUsernameChanged($body); + $this->task->username = $username; + $this->task->execute(mock(Queue::class)); /** @var MojangUsername|null $mojangUsername */ $mojangUsername = MojangUsername::findOne($username); $this->assertInstanceOf(MojangUsername::class, $mojangUsername); $this->assertNotEquals($mojangInfo->uuid, $mojangUsername->uuid); } - public function testRouteAccountBanned() { - /** @var \common\models\Account $bannedAccount */ - $bannedAccount = $this->tester->grabFixture('accounts', 'banned-account'); - $this->tester->haveFixtures([ - 'oauthSessions' => \tests\codeception\common\fixtures\OauthSessionFixture::class, - 'minecraftAccessKeys' => \tests\codeception\common\fixtures\MinecraftAccessKeyFixture::class, - 'authSessions' => \tests\codeception\common\fixtures\AccountSessionFixture::class, - ]); - - $body = new AccountBanned(); - $body->accountId = $bannedAccount->id; - - $this->controller->routeAccountBanned($body); - $this->assertEmpty($bannedAccount->sessions); - $this->assertEmpty($bannedAccount->minecraftAccessKeys); - $this->assertEmpty($bannedAccount->oauthSessions); - } - } diff --git a/tests/codeception/config/config.php b/tests/codeception/config/config.php index 78b097c..72c5fa3 100644 --- a/tests/codeception/config/config.php +++ b/tests/codeception/config/config.php @@ -20,9 +20,6 @@ return [ // Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается 'passwordHashCost' => 4, ], - 'amqp' => [ - 'class' => tests\codeception\common\_support\amqp\TestComponent::class, - ], 'queue' => [ 'class' => tests\codeception\common\_support\queue\Queue::class, ],