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).
This commit is contained in:
ErickSkrauch
2018-07-08 18:20:19 +03:00
parent 6751eb6591
commit c0aa78d156
55 changed files with 933 additions and 1684 deletions

View File

@ -1,91 +0,0 @@
<?php
namespace tests\codeception\common\_support\amqp;
use Codeception\Exception\ModuleException;
use Codeception\Module;
use Codeception\Module\Yii2;
class Helper extends Module {
/**
* Checks that message is created.
*
* ```php
* <?php
* // check that at least 1 message was created
* $I->seeAmqpMessageIsCreated();
*
* // check that only 3 messages were created
* $I->seeAmqpMessageIsCreated(3);
* ```
*
* @param string|null $exchange
* @param int|null $num
*/
public function seeAmqpMessageIsCreated($exchange = null, $num = null) {
if ($num === null) {
$this->assertNotEmpty($this->grabSentAmqpMessages($exchange), 'message were created');
return;
}
$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;
}
}

View File

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

View File

@ -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;
}
/**

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\fixtures;
use common\models\WebHookEvent;
use yii\test\ActiveFixture;
class WebHooksEventsFixture extends ActiveFixture {
public $modelClass = WebHookEvent::class;
public $dataFile = '@tests/codeception/common/fixtures/data/webhooks-events.php';
public $depends = [
WebHooksFixture::class,
];
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\fixtures;
use common\models\WebHook;
use yii\test\ActiveFixture;
class WebHooksFixture extends ActiveFixture {
public $modelClass = WebHook::class;
public $dataFile = '@tests/codeception/common/fixtures/data/webhooks.php';
}

View File

@ -0,0 +1,11 @@
<?php
return [
[
'webhook_id' => 1,
'event_type' => 'account.edit',
],
[
'webhook_id' => 2,
'event_type' => 'account.edit',
],
];

View File

@ -0,0 +1,21 @@
<?php
return [
'webhook-with-secret' => [
'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,
],
];

View File

@ -3,6 +3,7 @@ modules:
enabled:
- Yii2:
part: [orm, email, fixtures]
- tests\codeception\common\_support\queue\CodeceptionQueueHelper
- tests\codeception\common\_support\Mockery
config:
Yii2:

View File

@ -1,14 +1,20 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\models;
use Codeception\Specify;
use common\components\UserPass;
use common\models\Account;
use common\tasks\CreateWebHooksDeliveries;
use tests\codeception\common\fixtures\MojangUsernameFixture;
use tests\codeception\common\unit\TestCase;
use Yii;
use const common\LATEST_RULES_VERSION;
/**
* @covers \common\models\Account
*/
class AccountTest extends TestCase {
use Specify;
@ -119,4 +125,37 @@ class AccountTest extends TestCase {
$this->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);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\models\Account;
use common\tasks\ClearAccountSessions;
use tests\codeception\common\fixtures;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\ClearAccountSessions
*/
class ClearAccountSessionsTest extends TestCase {
public function _fixtures() {
return [
'accounts' => 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);
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\models\Account;
use common\tasks\CreateWebHooksDeliveries;
use common\tasks\DeliveryWebHook;
use tests\codeception\common\fixtures;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\CreateWebHooksDeliveries
*/
class CreateWebHooksDeliveriesTest extends TestCase {
public function _fixtures(): array {
return [
'webhooks' => 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);
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\tasks\DeliveryWebHook;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\DeliveryWebHook
*/
class DeliveryWebHookTest extends TestCase {
private $historyContainer = [];
/**
* @var Response|\GuzzleHttp\Exception\GuzzleException
*/
private $response;
public function testCanRetry() {
$task = new DeliveryWebHook();
$this->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;
}
};
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace tests\codeception\common\unit\tasks;
use common\components\Mojang\Api;
use common\components\Mojang\exceptions\NoContentException;
use common\components\Mojang\response\UsernameToUUIDResponse;
use common\models\Account;
use common\models\MojangUsername;
use common\tasks\PullMojangUsername;
use tests\codeception\common\fixtures\MojangUsernameFixture;
use tests\codeception\common\unit\TestCase;
use yii\queue\Queue;
/**
* @covers \common\tasks\PullMojangUsername
*/
class PullMojangUsernameTest extends TestCase {
private $expectedResponse;
/**
* @var PullMojangUsername
*/
private $task;
public function _fixtures() {
return [
'mojangUsernames' => MojangUsernameFixture::class,
];
}
public function _before() {
parent::_before();
/** @var PullMojangUsername|\PHPUnit_Framework_MockObject_MockObject $task */
$task = $this->getMockBuilder(PullMojangUsername::class)
->setMethods(['createMojangApi'])
->getMock();
/** @var Api|\PHPUnit_Framework_MockObject_MockObject $apiMock */
$apiMock = $this->getMockBuilder(Api::class)
->setMethods(['usernameToUUID'])
->getMock();
$apiMock
->expects($this->any())
->method('usernameToUUID')
->willReturnCallback(function() {
if ($this->expectedResponse === false) {
throw new NoContentException();
}
return $this->expectedResponse;
});
$task
->expects($this->any())
->method('createMojangApi')
->willReturn($apiMock);
$this->task = $task;
}
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\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 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;
$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 testExecuteRemoveIfExistsNoMore() {
$this->expectedResponse = false;
$username = $this->tester->grabFixture('mojangUsernames', 'not-exists')['username'];
$this->task->username = $username;
$this->task->execute(mock(Queue::class));
/** @var MojangUsername|null $mojangUsername */
$mojangUsername = MojangUsername::findOne($username);
$this->assertNull($mojangUsername);
}
public function testExecuteUuidUpdated() {
$expectedResponse = new UsernameToUUIDResponse();
$expectedResponse->id = 'f498513ce8c84773be26ecfc7ed5185d';
$expectedResponse->name = 'jeb';
$this->expectedResponse = $expectedResponse;
/** @var MojangUsername $mojangInfo */
$mojangInfo = $this->tester->grabFixture('mojangUsernames', 'uuid-changed');
$username = $mojangInfo['username'];
$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);
}
}