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, ],