Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class ConfigTypeFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testSendEmailDetailsToggleIsOnByDefault(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', '/s/config/edit');
|
||||
|
||||
// Updated CSS selector based on the new ID
|
||||
$yesSpan = $crawler->filter('#config_webhookconfig_webhook_email_details_label > div > span');
|
||||
|
||||
// Assert that exactly one such span exists
|
||||
Assert::assertCount(1, $yesSpan, 'The "Yes" span for "Send email details" toggle should exist.');
|
||||
|
||||
// Assert that the text within the span is "Yes"
|
||||
Assert::assertSame('Yes', $yesSpan->text(), 'The "Send email details" toggle should be set to "Yes" by default.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Functional\Command;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Command\DeleteWebhookLogsCommand;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class DeleteWebhookLogsCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var WebhookModel
|
||||
*/
|
||||
private $webhookModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['clean_webhook_logs_in_background'] = 'testRemoveLogUsingCleanUpJob' === $this->name();
|
||||
$this->configParams['webhook_log_max'] = 5;
|
||||
parent::setUp();
|
||||
|
||||
$this->webhookModel = static::getContainer()->get('mautic.webhook.model.webhook');
|
||||
}
|
||||
|
||||
public function testRemoveLogInstantly(): void
|
||||
{
|
||||
$webhook = $this->createWebhook('test', 'http://domain.tld', 'secret');
|
||||
$this->createWebhookEvent($webhook, 'Type');
|
||||
$logIds = [];
|
||||
for ($log = 1; $log <= 6; ++$log) {
|
||||
$addedLog = $this->createWebhookLog($webhook, 'test', 200);
|
||||
array_push($logIds, $addedLog->getId());
|
||||
}
|
||||
|
||||
$this->assertLogs($webhook, 6, $logIds);
|
||||
|
||||
$this->webhookModel->addLog($webhook, 200, 15);
|
||||
|
||||
array_shift($logIds);
|
||||
array_push($logIds, end($logIds) + 1);
|
||||
$this->assertLogs($webhook, 6, $logIds);
|
||||
}
|
||||
|
||||
public function testRemoveLogUsingCleanUpJob(): void
|
||||
{
|
||||
$webhook = $this->createWebhook('test', 'http://domain.tld', 'secret');
|
||||
$this->createWebhookEvent($webhook, 'Type');
|
||||
$logIds = [];
|
||||
for ($log = 1; $log <= 6; ++$log) {
|
||||
$addedLog = $this->createWebhookLog($webhook, 'test', 200);
|
||||
array_push($logIds, $addedLog->getId());
|
||||
}
|
||||
|
||||
$this->assertLogs($webhook, 6, $logIds);
|
||||
|
||||
$this->webhookModel->addLog($webhook, 200, 15);
|
||||
|
||||
array_push($logIds, end($logIds) + 1);
|
||||
$this->assertLogs($webhook, 7, $logIds);
|
||||
}
|
||||
|
||||
public function testRemoveLogCommand(): void
|
||||
{
|
||||
$webhook = $this->createWebhook('test', 'http://domain.tld', 'secret');
|
||||
$this->createWebhookEvent($webhook, 'Type');
|
||||
$logIds = [];
|
||||
for ($log = 1; $log <= 7; ++$log) {
|
||||
$addedLog = $this->createWebhookLog($webhook, 'test', 200);
|
||||
array_push($logIds, $addedLog->getId());
|
||||
}
|
||||
|
||||
$output = $this->testSymfonyCommand(DeleteWebhookLogsCommand::COMMAND_NAME);
|
||||
Assert::assertStringContainsString('2 logs deleted successfully for webhook id - '.$webhook->getId(), $output->getDisplay());
|
||||
array_shift($logIds);
|
||||
array_shift($logIds);
|
||||
$this->assertLogs($webhook, 5, $logIds);
|
||||
}
|
||||
|
||||
public function testRemoveLogCommandForNoWebhook(): void
|
||||
{
|
||||
$output = $this->testSymfonyCommand(DeleteWebhookLogsCommand::COMMAND_NAME);
|
||||
Assert::assertStringContainsString('There is 0 webhooks with logs more than defined limit.', $output->getDisplay());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $expectedIds
|
||||
*/
|
||||
private function assertLogs(Webhook $webhook, int $expectedCount, array $expectedIds): void
|
||||
{
|
||||
$logs = $this->em->getRepository(Log::class)->findBy(['webhook' => $webhook]);
|
||||
$logIds = array_map(fn (Log $log) => $log->getId(), $logs);
|
||||
|
||||
Assert::assertCount($expectedCount, $logs);
|
||||
Assert::assertSame($expectedIds, $logIds);
|
||||
}
|
||||
|
||||
private function createWebhook(string $name, string $url, string $secret): Webhook
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setName($name);
|
||||
$webhook->setWebhookUrl($url);
|
||||
$webhook->setSecret($secret);
|
||||
$this->em->persist($webhook);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
private function createWebhookEvent(Webhook $webhook, string $type): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setWebhook($webhook);
|
||||
$event->setEventType($type);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createWebhookLog(Webhook $webhook, string $note, int $statusCode): Log
|
||||
{
|
||||
$log = new Log();
|
||||
$log->setWebhook($webhook);
|
||||
$log->setNote($note);
|
||||
$log->setStatusCode($statusCode);
|
||||
$this->em->persist($log);
|
||||
$this->em->flush();
|
||||
|
||||
return $log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Functional\Command;
|
||||
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Test\Guzzle\ClientMockTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Command\ProcessWebhookQueuesCommand;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueue;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
final class ProcessWebhookQueuesCommandTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ClientMockTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['queue_mode'] = WebhookModel::COMMAND_PROCESS;
|
||||
$this->configParams['webhook_limit'] = 3;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testCommand(): void
|
||||
{
|
||||
$webhook = $this->createWebhook('test', 'https://httpbin.org/post', 'secret');
|
||||
$event = $this->createWebhookEvent($webhook, 'Type');
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
$queueIds = [];
|
||||
|
||||
// Generate 10 queue records.
|
||||
for ($i = 1; $i <= 10; ++$i) {
|
||||
$addedLog = $this->createWebhookQueue($webhook, $event, "Some payload {$i}");
|
||||
array_push($queueIds, $addedLog->getId());
|
||||
|
||||
$handlerStack->append(
|
||||
function (RequestInterface $request) {
|
||||
Assert::assertSame('POST', $request->getMethod());
|
||||
Assert::assertSame('https://httpbin.org/post', $request->getUri()->__toString());
|
||||
|
||||
return new Response(SymfonyResponse::HTTP_OK);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Process queue records from 4 to 9 including. 6 in total.
|
||||
$output = $this->testSymfonyCommand(
|
||||
ProcessWebhookQueuesCommand::COMMAND_NAME,
|
||||
['--webhook-id' => $webhook->getId(), '--min-id' => $queueIds[3], '--max-id' => $queueIds[8]]
|
||||
);
|
||||
Assert::assertStringContainsString('Webhook Processing Complete', $output->getDisplay());
|
||||
|
||||
// There will be 2 batches of webhook events sent. We've set we want to send 3 events per batch.
|
||||
Assert::assertCount(2, $this->em->getRepository(Log::class)->findBy(['webhook' => $webhook]));
|
||||
|
||||
// And 4 out of 10 queue records will be left alone as they did not fit the ID range.
|
||||
Assert::assertCount(4, $this->em->getRepository(WebhookQueue::class)->findBy(['webhook' => $webhook]));
|
||||
}
|
||||
|
||||
private function createWebhook(string $name, string $url, string $secret): Webhook
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setName($name);
|
||||
$webhook->setWebhookUrl($url);
|
||||
$webhook->setSecret($secret);
|
||||
$this->em->persist($webhook);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
private function createWebhookEvent(Webhook $webhook, string $type): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setWebhook($webhook);
|
||||
$event->setEventType($type);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createWebhookQueue(Webhook $webhook, Event $event, string $payload): WebhookQueue
|
||||
{
|
||||
$record = new WebhookQueue();
|
||||
$record->setWebhook($webhook);
|
||||
$record->setEvent($event);
|
||||
$record->setPayload($payload);
|
||||
$record->setDateAdded(new \DateTime());
|
||||
$this->em->persist($record);
|
||||
$this->em->flush();
|
||||
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class WebhookControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testViewWebhookDetail(): void
|
||||
{
|
||||
$webhook = $this->createWebhook('test', 'http://domain.tld', 'secret');
|
||||
$this->createWebhookEvent($webhook, 'Type');
|
||||
for ($log = 1; $log <= 105; ++$log) {
|
||||
$this->createWebhookLog($webhook, 'test', 200);
|
||||
}
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/webhooks/view/'.$webhook->getId());
|
||||
Assert::assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent());
|
||||
|
||||
$logList = $crawler->filter('.table.table-responsive > tbody > tr')->count();
|
||||
Assert::assertSame(Webhook::LOGS_DISPLAY_LIMIT, $logList);
|
||||
}
|
||||
|
||||
private function createWebhook(string $name, string $url, string $secret): Webhook
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setName($name);
|
||||
$webhook->setWebhookUrl($url);
|
||||
$webhook->setSecret($secret);
|
||||
$this->em->persist($webhook);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
private function createWebhookEvent(Webhook $webhook, string $type): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setWebhook($webhook);
|
||||
$event->setEventType($type);
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createWebhookLog(Webhook $webhook, string $note, int $statusCode): Log
|
||||
{
|
||||
$log = new Log();
|
||||
$log->setWebhook($webhook);
|
||||
$log->setNote($note);
|
||||
$log->setStatusCode($statusCode);
|
||||
$this->em->persist($log);
|
||||
|
||||
return $log;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueue;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class WebhookQueueFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPayloadCompressed(): void
|
||||
{
|
||||
$webhookQueue = $this->createWebhookQueue();
|
||||
|
||||
$payload = 'Compressed payload';
|
||||
$webhookQueue->setPayload($payload);
|
||||
|
||||
Assert::assertSame($payload, $webhookQueue->getPayload());
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$payloadDbValues = $this->fetchPayloadDbValues($webhookQueue);
|
||||
Assert::assertSame($payload, gzuncompress($payloadDbValues['payload_compressed']));
|
||||
|
||||
$this->em->clear();
|
||||
$webhookQueue = $this->em->getRepository(WebhookQueue::class)
|
||||
->find($webhookQueue->getId());
|
||||
|
||||
Assert::assertSame($payload, $webhookQueue->getPayload());
|
||||
}
|
||||
|
||||
private function createWebhookQueue(): WebhookQueue
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setName('Test');
|
||||
$webhook->setWebhookUrl('http://domain.tld');
|
||||
$webhook->setSecret('secret');
|
||||
$this->em->persist($webhook);
|
||||
|
||||
$even = new Event();
|
||||
$even->setWebhook($webhook);
|
||||
$even->setEventType('Type');
|
||||
$this->em->persist($even);
|
||||
|
||||
$webhookQueue = new WebhookQueue();
|
||||
$webhookQueue->setWebhook($webhook);
|
||||
$webhookQueue->setEvent($even);
|
||||
$this->em->persist($webhookQueue);
|
||||
|
||||
return $webhookQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function fetchPayloadDbValues(WebhookQueue $webhookQueue): array
|
||||
{
|
||||
$prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$query = sprintf('SELECT payload_compressed FROM %swebhook_queue WHERE id = ?', $prefix);
|
||||
|
||||
return $this->connection->executeQuery($query, [$webhookQueue->getId()])
|
||||
->fetchAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Functional\Model;
|
||||
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class WebhookModelProcessFailureTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var WebhookModel
|
||||
*/
|
||||
private $webhookModel;
|
||||
|
||||
/**
|
||||
* @var MockHandler
|
||||
*/
|
||||
private $clientMockHandler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['queue_mode'] = WebhookModel::IMMEDIATE_PROCESS;
|
||||
$this->configParams['disable_auto_unpublish'] = 'testDisableAutoUnpublishIsEnabled' === $this->name();
|
||||
parent::setUp();
|
||||
|
||||
$this->webhookModel = self::$kernel->getContainer()->get('mautic.webhook.model.webhook');
|
||||
$this->clientMockHandler = new MockHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $logStatusCodes
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataFailureWithPreviousLogs')]
|
||||
public function testFailureWithPreviousLogs(array $logStatusCodes, bool $expectedIsPublished, int $expectedNumberOfLogs): void
|
||||
{
|
||||
$this->clientMockHandler->append(new Response(401));
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setUnHealthySince(new \DateTimeImmutable());
|
||||
foreach ($logStatusCodes as $logStatusCode) {
|
||||
$this->createWebhookLog($webhook, $logStatusCode);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->processWebhook($webhook);
|
||||
|
||||
Assert::assertSame($expectedIsPublished, $webhook->getIsPublished());
|
||||
$this->assertNumberOfLogs($expectedNumberOfLogs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
public static function dataFailureWithPreviousLogs(): iterable
|
||||
{
|
||||
yield 'no previous logs' => [[], true, 1];
|
||||
yield 'at least one successful previous log' => [[200, 403], true, 3];
|
||||
yield 'all failed previous logs' => [[401, 403], false, 3];
|
||||
}
|
||||
|
||||
public function test404DoesNotProduceRedundantLog(): void
|
||||
{
|
||||
$this->clientMockHandler->append(new Response(404));
|
||||
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setUnHealthySince(new \DateTimeImmutable());
|
||||
$this->createWebhookLog($webhook, 401);
|
||||
|
||||
$this->em->flush();
|
||||
$this->processWebhook($webhook);
|
||||
|
||||
Assert::assertFalse($webhook->getIsPublished());
|
||||
$this->assertNumberOfLogs(2);
|
||||
}
|
||||
|
||||
public function testWebhookIsNotUnpublishedIfModifiedRecently(): void
|
||||
{
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setDateModified(new \DateTime('-1 day'));
|
||||
$this->createWebhookLog($webhook, 401);
|
||||
|
||||
$this->em->flush();
|
||||
$this->processWebhook($webhook);
|
||||
|
||||
Assert::assertTrue($webhook->getIsPublished());
|
||||
$this->assertNumberOfLogs(2);
|
||||
}
|
||||
|
||||
public function testWebhookIsUnpublishedIfNotModifiedRecently(): void
|
||||
{
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setUnHealthySince(new \DateTimeImmutable());
|
||||
$this->createWebhookLog($webhook, 401);
|
||||
|
||||
$this->em->flush();
|
||||
$this->processWebhook($webhook);
|
||||
|
||||
Assert::assertFalse($webhook->getIsPublished());
|
||||
$this->assertNumberOfLogs(2);
|
||||
}
|
||||
|
||||
public function testDisableAutoUnpublishIsEnabled(): void
|
||||
{
|
||||
$webhook = $this->createWebhook();
|
||||
$this->createWebhookLog($webhook, 401);
|
||||
|
||||
$this->em->flush();
|
||||
$this->processWebhook($webhook);
|
||||
|
||||
Assert::assertTrue($webhook->getIsPublished());
|
||||
$this->assertNumberOfLogs(2);
|
||||
}
|
||||
|
||||
private function createWebhook(): Webhook
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy([]);
|
||||
|
||||
$webhook = new Webhook();
|
||||
$webhook->setCreatedBy($user);
|
||||
$webhook->setName('Test');
|
||||
$webhook->setWebhookUrl('https://domain.tld');
|
||||
$webhook->setSecret('secret');
|
||||
$webhook->setDateModified(new \DateTime('-1 week'));
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
$webhook->setChanges([]);
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
private function createWebhookEvent(Webhook $webhook): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setWebhook($webhook);
|
||||
$event->setEventType('type');
|
||||
$this->em->persist($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createWebhookLog(Webhook $webhook, int $statusCode): void
|
||||
{
|
||||
$log = new Log();
|
||||
$log->setWebhook($webhook);
|
||||
$log->setStatusCode($statusCode);
|
||||
$this->em->persist($log);
|
||||
}
|
||||
|
||||
private function processWebhook(Webhook $webhook): void
|
||||
{
|
||||
$event = $this->createWebhookEvent($webhook);
|
||||
$queue = $this->webhookModel->queueWebhook($webhook, $event, []);
|
||||
$this->webhookModel->processWebhook($webhook, $queue);
|
||||
}
|
||||
|
||||
private function assertNumberOfLogs(int $expectedNumberOfLogs): void
|
||||
{
|
||||
Assert::assertSame($expectedNumberOfLogs, $this->em->getRepository(Log::class)->count([]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Functional\Model;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueue;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class WebhookModelTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Cleanup from previous tests
|
||||
$this->connection->executeStatement('DELETE FROM '.MAUTIC_TABLE_PREFIX.'webhook_queue');
|
||||
$this->connection->executeStatement('ALTER TABLE '.MAUTIC_TABLE_PREFIX.'webhook_queue AUTO_INCREMENT = 1');
|
||||
}
|
||||
|
||||
public function testEventsOrderByDirAsc(): void
|
||||
{
|
||||
$webhookModel = $this->getWebhookModel(Order::Ascending->value);
|
||||
$webhook = $this->createWebhookAndQueue();
|
||||
$queueArray = $webhookModel->getWebhookQueues($webhook);
|
||||
|
||||
// Order should be 1 to 10
|
||||
$counter = 1;
|
||||
|
||||
foreach ($queueArray as $queuedEvent) {
|
||||
Assert::assertSame((string) $counter, $queuedEvent->getId());
|
||||
|
||||
$payload = json_decode($queuedEvent->getPayload(), true);
|
||||
Assert::assertSame($counter, $payload['spoof']);
|
||||
|
||||
++$counter;
|
||||
}
|
||||
|
||||
Assert::assertSame(11, $counter);
|
||||
}
|
||||
|
||||
public function testEventsOrderByDirDesc(): void
|
||||
{
|
||||
$webhookModel = $this->getWebhookModel(Order::Descending->value);
|
||||
$webhook = $this->createWebhookAndQueue();
|
||||
$queueArray = $webhookModel->getWebhookQueues($webhook);
|
||||
|
||||
// Order should be 10 to 1
|
||||
$counter = 10;
|
||||
foreach ($queueArray as $queuedEvent) {
|
||||
Assert::assertSame((string) $counter, $queuedEvent->getId());
|
||||
|
||||
$payload = json_decode($queuedEvent->getPayload(), true);
|
||||
Assert::assertSame($counter, $payload['spoof']);
|
||||
|
||||
--$counter;
|
||||
}
|
||||
|
||||
Assert::assertSame(0, $counter);
|
||||
}
|
||||
|
||||
private function createWebhookAndQueue(): Webhook
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
|
||||
$webhook->setName('Test Webhook');
|
||||
$webhook->setWebhookUrl('https://localhost');
|
||||
$webhook->setSecret('abc13');
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
|
||||
$event = new Event();
|
||||
$event->setWebhook($webhook);
|
||||
$event->setEventType('mautic.email_on_send');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
$counter = 1;
|
||||
while ($counter <= 10) {
|
||||
$this->createWebhookQueue($webhook, $event, ['spoof' => $counter]);
|
||||
|
||||
++$counter;
|
||||
}
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $payload
|
||||
*/
|
||||
private function createWebhookQueue(Webhook $webhook, Event $event, array $payload): void
|
||||
{
|
||||
$queue = new WebhookQueue();
|
||||
$queue->setDateAdded(new \DateTime());
|
||||
$queue->setEvent($event);
|
||||
$queue->setWebhook($webhook);
|
||||
$queue->setPayload(json_encode($payload));
|
||||
$this->em->persist($queue);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function getWebhookModel(string $direction): WebhookModel
|
||||
{
|
||||
$webhookParams = [
|
||||
'queue_mode' => WebhookModel::COMMAND_PROCESS,
|
||||
'events_orderby_dir' => $direction,
|
||||
];
|
||||
|
||||
$this->setUpSymfony($webhookParams);
|
||||
|
||||
return static::getContainer()->get('mautic.webhook.model.webhook');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Functional;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use GuzzleHttp\Psr7\Response as GuzzleResponse;
|
||||
use Mautic\CoreBundle\Entity\Notification;
|
||||
use Mautic\CoreBundle\Entity\NotificationRepository;
|
||||
use Mautic\CoreBundle\Test\Guzzle\ClientMockTrait;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\WebhookBundle\Command\ProcessWebhookQueuesCommand;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueue;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueueRepository;
|
||||
use Mautic\WebhookBundle\Entity\WebhookRepository;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class WebhookFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ClientMockTrait;
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
/**
|
||||
* @var WebhookQueueRepository
|
||||
*/
|
||||
private $webhookQueueRepository;
|
||||
|
||||
/**
|
||||
* @var NotificationRepository
|
||||
*/
|
||||
private $notificationRepository;
|
||||
|
||||
/**
|
||||
* @var WebhookRepository|EntityRepository<Webhook>
|
||||
*/
|
||||
private $webhhokRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->authenticateApi = true;
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpSymfony(
|
||||
$this->configParams +
|
||||
[
|
||||
'queue_mode' => WebhookModel::COMMAND_PROCESS,
|
||||
'webhook_limit' => 2,
|
||||
]
|
||||
);
|
||||
|
||||
$this->truncateTables('leads', 'webhooks', 'webhook_queue', 'webhook_events');
|
||||
|
||||
$this->webhookQueueRepository = $this->em->getRepository(WebhookQueue::class);
|
||||
$this->notificationRepository = $this->em->getRepository(Notification::class);
|
||||
$this->webhhokRepository = $this->em->getRepository(Webhook::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after the tests.
|
||||
*/
|
||||
protected function beforeTearDown(): void
|
||||
{
|
||||
$this->truncateTables('leads', 'webhooks', 'webhook_queue', 'webhook_events');
|
||||
}
|
||||
|
||||
public function testWebhookWorkflowWithCommandProcess(): void
|
||||
{
|
||||
$webhookQueueRepository = $this->em->getRepository(WebhookQueue::class);
|
||||
\assert($webhookQueueRepository instanceof WebhookQueueRepository);
|
||||
$this->mockSuccessfulWebhookResponse(2);
|
||||
$webhook = $this->createWebhook();
|
||||
// Ensure we have a clean slate. There should be no rows waiting to be processed at this point.
|
||||
Assert::assertSame(0, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
|
||||
$this->createContacts();
|
||||
|
||||
// At this point there should be 3 events waiting to be processed.
|
||||
Assert::assertSame(3, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME, ['--webhook-id' => $webhook->getId()]);
|
||||
|
||||
// The queue should be processed now.
|
||||
Assert::assertSame(0, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
}
|
||||
|
||||
public function testWebhookWorkflowWithCommandProcessInQueueRange(): void
|
||||
{
|
||||
$this->mockSuccessfulWebhookResponse(2);
|
||||
$webhook = $this->createWebhook();
|
||||
$contacts = $this->createContacts();
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME, [
|
||||
'--webhook-id' => $webhook->getId(),
|
||||
'--min-id' => $contacts[0],
|
||||
'--max-id' => $contacts[2],
|
||||
]);
|
||||
|
||||
// The queue should be processed now.
|
||||
Assert::assertSame(0, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
}
|
||||
|
||||
public function testWebhookWorkflowWithCommandProcessWithoutPassingWebhookID(): void
|
||||
{
|
||||
$this->mockSuccessfulWebhookResponse(2);
|
||||
$webhook = $this->createWebhook();
|
||||
$this->createContacts();
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME);
|
||||
|
||||
// The queue should be processed now.
|
||||
Assert::assertSame(0, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
public static function dataNotificationToUser(): iterable
|
||||
{
|
||||
yield 'Support User' => [null, 1];
|
||||
yield 'Actual user' => [1, 1];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataNotificationToUser')]
|
||||
public function testWebhookFailureNotificationSent(?int $createdByUserId, ?int $expectedUserId): void
|
||||
{
|
||||
$this->mockFailedWebhookResponse(2);
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setCreatedBy();
|
||||
$webhook->setModifiedBy();
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
$this->createContacts();
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME, ['--webhook-id' => $webhook->getId()]);
|
||||
|
||||
Assert::assertSame(3, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
|
||||
$webhookQueues = $this->getWebhookQueue($webhook->getId());
|
||||
foreach ($webhookQueues as $webhookQueue) {
|
||||
$webhookQueue->setRetries(2);
|
||||
$webhookQueue->setDateModified((new \DateTimeImmutable())->modify('-3601 seconds'));
|
||||
$this->em->persist($webhookQueue);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$webhook->setCreatedBy($createdByUserId);
|
||||
$webhook->setModifiedBy($createdByUserId);
|
||||
$webhook->setUnHealthySince((new \DateTimeImmutable())->modify('-3601 seconds'));
|
||||
$webhook->setMarkedUnhealthyAt((new \DateTimeImmutable())->modify('-3601 seconds'));
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME, ['--webhook-id' => $webhook->getId()]);
|
||||
|
||||
Assert::assertCount(1, $this->notificationRepository->getNotifications($expectedUserId));
|
||||
Assert::assertSame(3, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME);
|
||||
|
||||
$webhook = $this->webhhokRepository->find($webhook->getId());
|
||||
Assert::assertNotNull($webhook->getMarkedUnhealthyAt());
|
||||
Assert::assertNotNull($webhook->getUnHealthySince());
|
||||
Assert::assertNotNull($webhook->getLastNotificationSentAt());
|
||||
}
|
||||
|
||||
public function testWebhookQueueNotProcessedIfMarkedUnhealthy(): void
|
||||
{
|
||||
$this->mockSuccessfulWebhookResponse();
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setMarkedUnhealthyAt(new \DateTimeImmutable());
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
$this->createContacts();
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME);
|
||||
|
||||
// The queue should not be processed.
|
||||
Assert::assertSame(3, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
}
|
||||
|
||||
public function testWebhookQueueProcessedWhenUnhealthyTimePassed(): void
|
||||
{
|
||||
$this->mockSuccessfulWebhookResponse(2);
|
||||
$webhook = $this->createWebhook();
|
||||
$webhook->setMarkedUnhealthyAt((new \DateTimeImmutable())->modify('-301 seconds'));
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
$this->createContacts();
|
||||
|
||||
$this->testSymfonyCommand(ProcessWebhookQueuesCommand::COMMAND_NAME);
|
||||
|
||||
$webhook = $this->webhhokRepository->find($webhook->getId());
|
||||
Assert::assertNull($webhook->getMarkedUnhealthyAt());
|
||||
Assert::assertNull($webhook->getUnHealthySince());
|
||||
Assert::assertNull($webhook->getLastNotificationSentAt());
|
||||
|
||||
// The queue should be processed.
|
||||
Assert::assertSame(0, $this->getQueueCountByWebhookId($webhook->getId()));
|
||||
}
|
||||
|
||||
private function createWebhook(): Webhook
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$event = new Event();
|
||||
|
||||
$event->setEventType('mautic.lead_post_save_new');
|
||||
$event->setWebhook($webhook);
|
||||
|
||||
$webhook->addEvent($event);
|
||||
$webhook->setName('Webhook from a functional test');
|
||||
$webhook->setWebhookUrl('https://httpbin.org/post');
|
||||
$webhook->setSecret('any_secret_will_do');
|
||||
$webhook->isPublished(true);
|
||||
$webhook->setCreatedBy(1);
|
||||
|
||||
$this->em->persist($event);
|
||||
$this->em->persist($webhook);
|
||||
$this->em->flush();
|
||||
|
||||
return $webhook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating some contacts via API so all the listeners are triggered.
|
||||
* It's closer to a real world contact creation.
|
||||
*/
|
||||
private function createContacts(): array
|
||||
{
|
||||
$contacts = [
|
||||
[
|
||||
'email' => sprintf('contact1%s@email.com', mt_rand(99999, 999999)),
|
||||
'firstname' => 'Contact',
|
||||
'lastname' => 'One',
|
||||
'points' => 4,
|
||||
'city' => 'Houston',
|
||||
'state' => 'Texas',
|
||||
'country' => 'United States',
|
||||
],
|
||||
[
|
||||
'email' => sprintf('contact2%s@email.com', mt_rand(99999, 999999)),
|
||||
'firstname' => 'Contact',
|
||||
'lastname' => 'Two',
|
||||
'city' => 'Boston',
|
||||
'state' => 'Massachusetts',
|
||||
'country' => 'United States',
|
||||
'timezone' => 'America/New_York',
|
||||
],
|
||||
[
|
||||
'email' => sprintf('contact3%s@email.com', mt_rand(99999, 999999)),
|
||||
'firstname' => 'contact',
|
||||
'lastname' => 'Three',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/contacts/batch/new', $contacts);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0], $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][1], $clientResponse->getContent());
|
||||
Assert::assertEquals(Response::HTTP_CREATED, $response['statusCodes'][2], $clientResponse->getContent());
|
||||
|
||||
return [
|
||||
$response['contacts'][0]['id'],
|
||||
$response['contacts'][1]['id'],
|
||||
$response['contacts'][2]['id'],
|
||||
];
|
||||
}
|
||||
|
||||
private function mockSuccessfulWebhookResponse(int $expectedToBeCalled = 0): void
|
||||
{
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
for (; $expectedToBeCalled > 0; --$expectedToBeCalled) {
|
||||
$handlerStack->append(
|
||||
function (RequestInterface $request) use (&$sendRequestCounter) {
|
||||
Assert::assertSame('/post', $request->getUri()->getPath());
|
||||
$jsonPayload = json_decode($request->getBody()->getContents(), true);
|
||||
Assert::assertNotEmpty($request->getHeader('Webhook-Signature'));
|
||||
|
||||
++$sendRequestCounter;
|
||||
|
||||
return new GuzzleResponse(Response::HTTP_OK);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function mockFailedWebhookResponse(int $expectedToBeCalled = 0): void
|
||||
{
|
||||
$handlerStack = $this->getClientMockHandler();
|
||||
for (; $expectedToBeCalled > 0; --$expectedToBeCalled) {
|
||||
$handlerStack->append(
|
||||
function (RequestInterface $request) use (&$sendRequestCounter) {
|
||||
Assert::assertSame('/post', $request->getUri()->getPath());
|
||||
$jsonPayload = json_decode($request->getBody()->getContents(), true);
|
||||
Assert::assertNotEmpty($request->getHeader('Webhook-Signature'));
|
||||
|
||||
++$sendRequestCounter;
|
||||
|
||||
return new GuzzleResponse(Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getWebhookQueue(int $webhookId): Paginator
|
||||
{
|
||||
return $this->webhookQueueRepository->getEntities([
|
||||
'webhook_id' => $webhookId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getQueueCountByWebhookId(int $webhookId): int
|
||||
{
|
||||
return $this->webhookQueueRepository->count([
|
||||
'webhook' => $webhookId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Entity;
|
||||
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
|
||||
class LogTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testSetNote(): void
|
||||
{
|
||||
$log = new Log();
|
||||
$log->setNote("\x6d\x61\x75\x74\x69\x63");
|
||||
$this->assertSame('mautic', $log->getNote());
|
||||
|
||||
$log->setNote("\x57\xfc\x72\x74\x74\x65\x6d\x62\x65\x72\x67"); // original string is W<>rttemberg, in this '<27>' is invaliad char so it should be removed
|
||||
$this->assertSame('Wrttemberg', $log->getNote());
|
||||
|
||||
$log->setNote('mautic');
|
||||
$this->assertSame('mautic', $log->getNote());
|
||||
|
||||
$log->setNote('ěščřžýá');
|
||||
$this->assertSame('ěščřžýá', $log->getNote());
|
||||
|
||||
$log->setNote('†º5¶2KfNœã');
|
||||
$this->assertSame('†º5¶2KfNœã', $log->getNote());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Entity;
|
||||
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class WebhookTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testWasModifiedRecentlyWithNotModifiedWebhook(): void
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$this->assertNull($webhook->getDateModified());
|
||||
$this->assertFalse($webhook->wasModifiedRecently());
|
||||
}
|
||||
|
||||
public function testWasModifiedRecentlyWithWebhookModifiedAWhileBack(): void
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setDateModified((new \DateTime())->modify('-20 days'));
|
||||
$this->assertFalse($webhook->wasModifiedRecently());
|
||||
}
|
||||
|
||||
public function testWasModifiedRecentlyWithWebhookModifiedRecently(): void
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$webhook->setDateModified((new \DateTime())->modify('-2 hours'));
|
||||
$this->assertTrue($webhook->wasModifiedRecently());
|
||||
}
|
||||
|
||||
public function testTriggersFromApiAreStoredAsEvents(): void
|
||||
{
|
||||
$webhook = new Webhook();
|
||||
$triggers = [
|
||||
'mautic.company_post_save',
|
||||
'mautic.company_post_delete',
|
||||
'mautic.lead_channel_subscription_changed',
|
||||
];
|
||||
|
||||
$webhook->setTriggers($triggers);
|
||||
|
||||
$events = $webhook->getEvents();
|
||||
Assert::assertCount(3, $events);
|
||||
|
||||
foreach ($events as $key => $event) {
|
||||
Assert::assertEquals($event->getEventType(), $triggers[$key]);
|
||||
Assert::assertSame($webhook, $event->getWebhook());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Event\WebhookEvent;
|
||||
use Mautic\WebhookBundle\EventListener\WebhookSubscriber;
|
||||
use Mautic\WebhookBundle\Notificator\WebhookKillNotificator;
|
||||
use Mautic\WebhookBundle\WebhookEvents;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class WebhookSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&IpLookupHelper
|
||||
*/
|
||||
private MockObject $ipLookupHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject&AuditLogModel
|
||||
*/
|
||||
private MockObject $auditLogModel;
|
||||
|
||||
/**
|
||||
* @var MockObject&WebhookKillNotificator
|
||||
*/
|
||||
private MockObject $webhookKillNotificator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$this->auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$this->webhookKillNotificator = $this->createMock(WebhookKillNotificator::class);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
[
|
||||
WebhookEvents::WEBHOOK_POST_SAVE => ['onWebhookSave', 0],
|
||||
WebhookEvents::WEBHOOK_POST_DELETE => ['onWebhookDelete', 0],
|
||||
WebhookEvents::WEBHOOK_KILL => ['onWebhookKill', 0],
|
||||
],
|
||||
WebhookSubscriber::getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testOnWebhookKill(): void
|
||||
{
|
||||
$webhookMock = $this->createMock(Webhook::class);
|
||||
$reason = 'reason';
|
||||
|
||||
$eventMock = $this->createMock(WebhookEvent::class);
|
||||
$eventMock
|
||||
->expects($this->once())
|
||||
->method('getWebhook')
|
||||
->willReturn($webhookMock);
|
||||
$eventMock
|
||||
->expects($this->once())
|
||||
->method('getReason')
|
||||
->willReturn($reason);
|
||||
|
||||
$this->webhookKillNotificator
|
||||
->expects($this->once())
|
||||
->method('send')
|
||||
->with($webhookMock, $reason);
|
||||
|
||||
$subscriber = new WebhookSubscriber($this->ipLookupHelper, $this->auditLogModel, $this->webhookKillNotificator);
|
||||
$subscriber->onWebhookKill($eventMock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\CompanyModel;
|
||||
use Mautic\WebhookBundle\Helper\CampaignHelper;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class CampaignHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&Lead
|
||||
*/
|
||||
private MockObject $contact;
|
||||
|
||||
/**
|
||||
* @var MockObject|Client
|
||||
*/
|
||||
private MockObject $client;
|
||||
|
||||
/**
|
||||
* @var MockObject|CompanyModel
|
||||
*/
|
||||
private MockObject $companyModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|CompanyRepository
|
||||
*/
|
||||
private MockObject $companyRepository;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int,IpAddress>
|
||||
*/
|
||||
private ArrayCollection $ipCollection;
|
||||
|
||||
private CampaignHelper $campaignHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $dispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->contact = $this->createMock(Lead::class);
|
||||
$this->client = $this->createMock(Client::class);
|
||||
$this->companyModel = $this->createMock(CompanyModel::class);
|
||||
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->ipCollection = new ArrayCollection();
|
||||
$this->companyRepository = $this->getMockBuilder(CompanyRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getCompaniesByLeadId'])
|
||||
->getMock();
|
||||
|
||||
$this->companyRepository->method('getCompaniesByLeadId')->willReturn([new Company()]);
|
||||
|
||||
$this->companyModel->method('getRepository')->willReturn($this->companyRepository);
|
||||
|
||||
$this->campaignHelper = new CampaignHelper($this->client, $this->companyModel, $this->dispatcher);
|
||||
|
||||
$this->ipCollection->add((new IpAddress())->setIpAddress('127.0.0.1'));
|
||||
$this->ipCollection->add((new IpAddress())->setIpAddress('127.0.0.2'));
|
||||
|
||||
$this->contact->expects($this->once())
|
||||
->method('getProfileFields')
|
||||
->willReturn(['email' => 'john@doe.email', 'company' => 'Mautic']);
|
||||
|
||||
$this->contact->expects($this->once())
|
||||
->method('getIpAddresses')
|
||||
->willReturn($this->ipCollection);
|
||||
}
|
||||
|
||||
public function testFireWebhookWithGet(): void
|
||||
{
|
||||
$expectedUrl = 'https://mautic.org?test=tee&email=john%40doe.email&IP=127.0.0.1%2C127.0.0.2';
|
||||
|
||||
$this->client->expects($this->once())
|
||||
->method('get')
|
||||
->with($expectedUrl, [
|
||||
\GuzzleHttp\RequestOptions::HEADERS => ['test' => 'tee', 'company' => 'Mautic'],
|
||||
\GuzzleHttp\RequestOptions::TIMEOUT => 10,
|
||||
])
|
||||
->willReturn(new Response(200));
|
||||
|
||||
$this->campaignHelper->fireWebhook($this->provideSampleConfig(), $this->contact);
|
||||
}
|
||||
|
||||
public function testFireWebhookWithPost(): void
|
||||
{
|
||||
$config = $this->provideSampleConfig('post');
|
||||
|
||||
$this->client->expects($this->once())
|
||||
->method('request')
|
||||
->with('post', 'https://mautic.org', [
|
||||
\GuzzleHttp\RequestOptions::FORM_PARAMS => ['test' => 'tee', 'email' => 'john@doe.email', 'IP' => '127.0.0.1,127.0.0.2'],
|
||||
\GuzzleHttp\RequestOptions::HEADERS => ['test' => 'tee', 'company' => 'Mautic'],
|
||||
\GuzzleHttp\RequestOptions::TIMEOUT => 10,
|
||||
])
|
||||
->willReturn(new Response(200));
|
||||
|
||||
$this->campaignHelper->fireWebhook($config, $this->contact);
|
||||
}
|
||||
|
||||
public function testFireWebhookWithPostJson(): void
|
||||
{
|
||||
$config = $this->provideSampleConfig('post', 'application/json');
|
||||
$this->client->expects($this->once())
|
||||
->method('request')
|
||||
->with('post', 'https://mautic.org', [
|
||||
\GuzzleHttp\RequestOptions::HEADERS => [
|
||||
'test' => 'tee',
|
||||
'company' => 'Mautic',
|
||||
'content-type' => 'application/json',
|
||||
],
|
||||
\GuzzleHttp\RequestOptions::TIMEOUT => 10,
|
||||
\GuzzleHttp\RequestOptions::BODY => json_encode(
|
||||
['test' => 'tee', 'email' => 'john@doe.email', 'IP' => '127.0.0.1,127.0.0.2']
|
||||
),
|
||||
])
|
||||
->willReturn(new Response(200));
|
||||
|
||||
$this->campaignHelper->fireWebhook($config, $this->contact);
|
||||
}
|
||||
|
||||
public function testFireWebhookWhenReturningNotFound(): void
|
||||
{
|
||||
$this->client->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn(new Response(404));
|
||||
|
||||
$this->expectException(\OutOfRangeException::class);
|
||||
|
||||
$this->campaignHelper->fireWebhook($this->provideSampleConfig(), $this->contact);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function provideSampleConfig(string $method = 'get', string $type = 'application/x-www-form-urlencoded'): array
|
||||
{
|
||||
$sample = [
|
||||
'url' => 'https://mautic.org',
|
||||
'method' => $method,
|
||||
'timeout' => 10,
|
||||
'additional_data' => [
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'test',
|
||||
'value' => 'tee',
|
||||
],
|
||||
[
|
||||
'label' => 'email',
|
||||
'value' => '{contactfield=email}',
|
||||
],
|
||||
[
|
||||
'label' => 'IP',
|
||||
'value' => '{contactfield=ipAddress}',
|
||||
],
|
||||
],
|
||||
],
|
||||
'headers' => [
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'test',
|
||||
'value' => 'tee',
|
||||
],
|
||||
[
|
||||
'label' => 'company',
|
||||
'value' => '{contactfield=company}',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
if ('application/json' == $type) {
|
||||
array_push($sample['headers']['list'],
|
||||
[
|
||||
'label' => 'content-type',
|
||||
'value' => 'application/json',
|
||||
]);
|
||||
}
|
||||
|
||||
return $sample;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Http;
|
||||
|
||||
use GuzzleHttp\Client as GuzzleClient;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PrivateAddressChecker;
|
||||
use Mautic\WebhookBundle\Http\Client;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ClientTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&CoreParametersHelper
|
||||
*/
|
||||
private MockObject $parametersMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&GuzzleClient
|
||||
*/
|
||||
private MockObject $httpClientMock;
|
||||
|
||||
private Client $client;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->parametersMock = $this->createMock(CoreParametersHelper::class);
|
||||
$this->httpClientMock = $this->createMock(GuzzleClient::class);
|
||||
$this->client = new Client($this->parametersMock, $this->httpClientMock, new PrivateAddressChecker());
|
||||
}
|
||||
|
||||
public function testPost(): void
|
||||
{
|
||||
$method = 'POST';
|
||||
$url = 'https://8.8.8.8';
|
||||
$payload = ['payload'];
|
||||
$secret = 'secret123';
|
||||
$siteUrl = 'siteUrl';
|
||||
|
||||
// Calculate the expected signature the same way as the Client class
|
||||
$jsonPayload = json_encode($payload);
|
||||
$expectedSignature = base64_encode(hash_hmac('sha256', $jsonPayload, $secret, true));
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Origin-Base-URL' => $siteUrl,
|
||||
'Webhook-Signature' => $expectedSignature,
|
||||
];
|
||||
|
||||
$response = new Response();
|
||||
|
||||
$matcher = $this->exactly(2);
|
||||
$this->parametersMock->expects($matcher)
|
||||
->method('get')
|
||||
->willReturnCallback(function (string $parameter) use ($matcher, $siteUrl) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('site_url', $parameter);
|
||||
|
||||
return $siteUrl;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('webhook_allowed_private_addresses', $parameter);
|
||||
|
||||
return [];
|
||||
}
|
||||
throw new \RuntimeException('Unexpected method call');
|
||||
});
|
||||
|
||||
$this->httpClientMock->expects($this->once())
|
||||
->method('sendRequest')
|
||||
->with($this->callback(function (Request $request) use ($method, $url, $headers, $payload) {
|
||||
$this->assertSame($method, $request->getMethod());
|
||||
$this->assertSame($url, (string) $request->getUri());
|
||||
|
||||
foreach ($headers as $headerName => $headerValue) {
|
||||
$header = $request->getHeader($headerName);
|
||||
$this->assertSame($headerValue, $header[0]);
|
||||
}
|
||||
|
||||
$this->assertSame(json_encode($payload), (string) $request->getBody());
|
||||
|
||||
return true;
|
||||
}))
|
||||
->willReturn($response);
|
||||
|
||||
$this->assertEquals($response, $this->client->post($url, $payload, $secret));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use JMS\Serializer\SerializerInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\WebhookBundle\Entity\Event;
|
||||
use Mautic\WebhookBundle\Entity\Log;
|
||||
use Mautic\WebhookBundle\Entity\LogRepository;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueue;
|
||||
use Mautic\WebhookBundle\Entity\WebhookQueueRepository;
|
||||
use Mautic\WebhookBundle\Entity\WebhookRepository;
|
||||
use Mautic\WebhookBundle\Http\Client;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use Mautic\WebhookBundle\Service\WebhookService;
|
||||
use Monolog\Logger;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGenerator;
|
||||
|
||||
class WebhookModelTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&CoreParametersHelper
|
||||
*/
|
||||
private MockObject $parametersHelperMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&SerializerInterface
|
||||
*/
|
||||
private MockObject $serializerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&EntityManager
|
||||
*/
|
||||
private MockObject $entityManagerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&WebhookRepository
|
||||
*/
|
||||
private MockObject $webhookRepository;
|
||||
|
||||
/**
|
||||
* @var MockObject&WebhookQueueRepository
|
||||
*/
|
||||
private $webhookQueueRepository;
|
||||
|
||||
/**
|
||||
* @var MockObject&UserHelper
|
||||
*/
|
||||
private MockObject $userHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject&EventDispatcherInterface
|
||||
*/
|
||||
private MockObject $eventDispatcherMock;
|
||||
|
||||
private WebhookModel $model;
|
||||
|
||||
/**
|
||||
* @var MockObject&Client
|
||||
*/
|
||||
private MockObject $httpClientMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->parametersHelperMock = $this->createMock(CoreParametersHelper::class);
|
||||
$this->serializerMock = $this->createMock(SerializerInterface::class);
|
||||
$this->entityManagerMock = $this->createMock(EntityManager::class);
|
||||
$this->userHelper = $this->createMock(UserHelper::class);
|
||||
$this->webhookRepository = $this->createMock(WebhookRepository::class);
|
||||
$this->webhookQueueRepository = $this->createMock(WebhookQueueRepository::class);
|
||||
$this->httpClientMock = $this->createMock(Client::class);
|
||||
$this->eventDispatcherMock = $this->createMock(EventDispatcher::class);
|
||||
|
||||
$this->model = $this->initModel();
|
||||
}
|
||||
|
||||
public function testSaveEntity(): void
|
||||
{
|
||||
$entity = new Webhook();
|
||||
|
||||
// The secret hash is null at first.
|
||||
$this->assertNull($entity->getSecret());
|
||||
|
||||
$this->entityManagerMock->expects($this->once())
|
||||
->method('getRepository')
|
||||
->with(Webhook::class)
|
||||
->willReturn($this->webhookRepository);
|
||||
|
||||
$this->webhookRepository->expects($this->once())
|
||||
->method('saveEntity')
|
||||
->with($this->callback(function (Webhook $entity) {
|
||||
// The secret hash is not empty on save.
|
||||
$this->assertNotEmpty($entity->getSecret());
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
$this->model->saveEntity($entity);
|
||||
}
|
||||
|
||||
public function testGetEventsOrderbyDirWhenSetInWebhook(): void
|
||||
{
|
||||
$webhook = (new Webhook())->setEventsOrderbyDir('DESC');
|
||||
$this->assertEquals('DESC', $this->model->getEventsOrderbyDir($webhook));
|
||||
}
|
||||
|
||||
public function testGetEventsOrderbyDirWhenNotSetInWebhook(): void
|
||||
{
|
||||
$this->parametersHelperMock->method('get')->willReturn('DESC');
|
||||
$this->assertEquals('DESC', $this->initModel()->getEventsOrderbyDir());
|
||||
}
|
||||
|
||||
public function testGetEventsOrderbyDirWhenWebhookNotProvided(): void
|
||||
{
|
||||
$this->parametersHelperMock->method('get')->willReturn('DESC');
|
||||
$this->assertEquals('DESC', $this->initModel()->getEventsOrderbyDir());
|
||||
}
|
||||
|
||||
public function testGetWebhookPayloadForPayloadInWebhook(): void
|
||||
{
|
||||
$payload = ['the' => 'payload'];
|
||||
$webhook = new Webhook();
|
||||
$webhook->setPayload($payload);
|
||||
|
||||
$this->assertEquals($payload, $this->model->getWebhookPayload($webhook));
|
||||
}
|
||||
|
||||
public function testGetWebhookPayloadForQueueLoadedFromDatabase(): void
|
||||
{
|
||||
$queueMock = $this->createMock(WebhookQueue::class);
|
||||
$webhook = new Webhook();
|
||||
$event = new Event();
|
||||
$event->setEventType('leads');
|
||||
$queueMock->method('getPayload')->willReturn('{"the": "payload"}');
|
||||
$queueMock->method('getEvent')->willReturn($event);
|
||||
$queueMock->method('getDateAdded')->willReturn(new \DateTime('2018-04-10T15:04:57+00:00'));
|
||||
$queueMock->method('getId')->willReturn(12);
|
||||
|
||||
$queueRepositoryMock = $this->createMock(WebhookQueueRepository::class);
|
||||
|
||||
$this->parametersHelperMock->method('get')
|
||||
->willReturnCallback(function ($param) {
|
||||
if ('queue_mode' === $param) {
|
||||
return WebhookModel::COMMAND_PROCESS;
|
||||
}
|
||||
if ('webhook_retry_delay' === $param) {
|
||||
return 3600;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$this->entityManagerMock->expects($this->once())
|
||||
->method('getRepository')
|
||||
->with(WebhookQueue::class)
|
||||
->willReturn($queueRepositoryMock);
|
||||
|
||||
$this->entityManagerMock->expects($this->once())
|
||||
->method('detach')
|
||||
->with($queueMock);
|
||||
|
||||
$queueRepositoryMock->expects($this->once())
|
||||
->method('getEntities')
|
||||
->willReturn([$queueMock]);
|
||||
|
||||
$expectedPayload = [
|
||||
'leads' => [
|
||||
[
|
||||
'the' => 'payload',
|
||||
'timestamp' => '2018-04-10T15:04:57+00:00',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expectedPayload, $this->initModel()->getWebhookPayload($webhook));
|
||||
}
|
||||
|
||||
public function testGetWebhookPayloadForQueueInWebhook(): void
|
||||
{
|
||||
$queue = new WebhookQueue();
|
||||
$webhook = new Webhook();
|
||||
$event = new Event();
|
||||
$event->setEventType('leads');
|
||||
$queue->setPayload('{"the": "payload"}');
|
||||
$queue->setEvent($event);
|
||||
$queue->setDateAdded(new \DateTime('2018-04-10T15:04:57+00:00'));
|
||||
|
||||
$this->parametersHelperMock->method('get')
|
||||
->willReturnCallback(function ($param) {
|
||||
if ('queue_mode' === $param) {
|
||||
return WebhookModel::IMMEDIATE_PROCESS;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$expectedPayload = [
|
||||
'leads' => [
|
||||
[
|
||||
'the' => 'payload',
|
||||
'timestamp' => '2018-04-10T15:04:57+00:00',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expectedPayload, $this->initModel()->getWebhookPayload($webhook, $queue));
|
||||
}
|
||||
|
||||
public function testProcessWebhook(): void
|
||||
{
|
||||
$webhook = new class extends Webhook {
|
||||
public function getId(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
$webhook->setWebhookUrl('test-webhook.com');
|
||||
|
||||
$event = new Event();
|
||||
$event->setEventType('mautic.email_on_send');
|
||||
|
||||
$queue = new class extends WebhookQueue {
|
||||
public function getId(): string
|
||||
{
|
||||
return '1';
|
||||
}
|
||||
};
|
||||
$queue->setPayload('{"payload": "some data"}');
|
||||
$queue->setEvent($event);
|
||||
$queue->setDateAdded(new \DateTime('2021-04-01T16:00:00+00:00'));
|
||||
|
||||
$webhookQueueRepoMock = $this->createMock(WebhookQueueRepository::class);
|
||||
$webhookLogRepoMock = $this->createMock(LogRepository::class);
|
||||
$webhookRepoMock = $this->createMock(WebhookRepository::class);
|
||||
|
||||
$this->entityManagerMock->method('getRepository')
|
||||
->willReturnMap([
|
||||
[WebhookQueue::class, $webhookQueueRepoMock],
|
||||
[Log::class, $webhookLogRepoMock],
|
||||
[Webhook::class, $webhookRepoMock],
|
||||
]);
|
||||
|
||||
$webhookQueueRepoMock
|
||||
->method('deleteQueuesById')
|
||||
->with([1]);
|
||||
|
||||
$responsePayload = [
|
||||
'mautic.email_on_send' => [
|
||||
[
|
||||
'payload' => 'some data',
|
||||
'timestamp' => '2021-04-01T16:00:00+00:00',
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->httpClientMock
|
||||
->method('post')
|
||||
->with('test-webhook.com', $responsePayload)
|
||||
->willReturn(new Response(200, [], 'Success'));
|
||||
|
||||
self::assertTrue($this->model->processWebhook($webhook, $queue));
|
||||
}
|
||||
|
||||
public function testMinAndMaxQueueIdWhenNoneIsSet(): void
|
||||
{
|
||||
$webhook = new class extends Webhook {
|
||||
public function getId(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
$webhook->setEventsOrderbyDir('ASC');
|
||||
|
||||
$this->entityManagerMock->expects($this->once())
|
||||
->method('getRepository')
|
||||
->with(WebhookQueue::class)
|
||||
->willReturn($this->webhookQueueRepository);
|
||||
|
||||
$this->webhookQueueRepository->method('getTableAlias')->willReturn('w');
|
||||
|
||||
$webhookRetryTime = (new \DateTimeImmutable())
|
||||
->format(DateTimeHelper::FORMAT_DB);
|
||||
$this->webhookQueueRepository->expects($this->once())
|
||||
->method('getEntities')
|
||||
->with(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'IDENTITY(w.webhook)',
|
||||
'expr' => 'eq',
|
||||
'value' => 1,
|
||||
],
|
||||
],
|
||||
'where' => [
|
||||
[
|
||||
'expr' => 'andX',
|
||||
'val' => [
|
||||
[
|
||||
'expr' => 'orX',
|
||||
'val' => [
|
||||
[
|
||||
'column' => 'w.retries',
|
||||
'expr' => 'eq',
|
||||
'value' => 0,
|
||||
],
|
||||
[
|
||||
'expr' => 'andX',
|
||||
'val' => [
|
||||
[
|
||||
'column' => 'w.retries',
|
||||
'expr' => 'gt',
|
||||
'value' => 0,
|
||||
],
|
||||
[
|
||||
'column' => 'w.dateModified',
|
||||
'expr' => 'lt',
|
||||
'value' => $webhookRetryTime,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'limit' => 0,
|
||||
'iterable_mode' => true,
|
||||
'start' => 0,
|
||||
'orderBy' => 'w.retries,w.id',
|
||||
'orderByDir' => 'ASC',
|
||||
]
|
||||
);
|
||||
$this->initModel()->getWebhookQueues($webhook);
|
||||
}
|
||||
|
||||
public function testMinAndMaxQueueIdWhenBothSet(): void
|
||||
{
|
||||
$webhook = new class extends Webhook {
|
||||
public function getId(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
$webhook->setEventsOrderbyDir('ASC');
|
||||
|
||||
$this->entityManagerMock->expects($this->once())
|
||||
->method('getRepository')
|
||||
->with(WebhookQueue::class)
|
||||
->willReturn($this->webhookQueueRepository);
|
||||
|
||||
$this->webhookQueueRepository->method('getTableAlias')->willReturn('w');
|
||||
$webhookRetryTime = (new \DateTimeImmutable())
|
||||
->format(DateTimeHelper::FORMAT_DB);
|
||||
$expected = [
|
||||
'iterable_mode' => true,
|
||||
'orderBy' => 'w.retries,w.id',
|
||||
'orderByDir' => 'ASC',
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'IDENTITY(w.webhook)',
|
||||
'expr' => 'eq',
|
||||
'value' => 1,
|
||||
],
|
||||
],
|
||||
'where' => [
|
||||
[
|
||||
'expr' => 'andX',
|
||||
'val' => [
|
||||
[
|
||||
'expr' => 'orX',
|
||||
'val' => [
|
||||
[
|
||||
'column' => 'w.retries',
|
||||
'expr' => 'eq',
|
||||
'value' => 0,
|
||||
],
|
||||
[
|
||||
'expr' => 'andX',
|
||||
'val' => [
|
||||
[
|
||||
'column' => 'w.retries',
|
||||
'expr' => 'gt',
|
||||
'value' => 0,
|
||||
],
|
||||
[
|
||||
'column' => 'w.dateModified',
|
||||
'expr' => 'lt',
|
||||
'value' => $webhookRetryTime,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'column' => 'w.id',
|
||||
'expr' => 'gte',
|
||||
'value' => 20,
|
||||
],
|
||||
[
|
||||
'column' => 'w.id',
|
||||
'expr' => 'lte',
|
||||
'value' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->webhookQueueRepository->expects($this->once())
|
||||
->method('getEntities')
|
||||
->with($expected);
|
||||
|
||||
$model = $this->initModel();
|
||||
$model->setMinQueueId(20);
|
||||
$model->setMaxQueueId(30);
|
||||
$model->getWebhookQueues($webhook);
|
||||
}
|
||||
|
||||
private function initModel(): WebhookModel
|
||||
{
|
||||
$webhookServiceMock = $this->createMock(WebhookService::class);
|
||||
|
||||
// create anew webhook model instance using mocks
|
||||
$model = new WebhookModel(
|
||||
$this->parametersHelperMock,
|
||||
$this->serializerMock,
|
||||
$this->httpClientMock,
|
||||
$this->entityManagerMock,
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->eventDispatcherMock,
|
||||
$this->createMock(UrlGenerator::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->userHelper,
|
||||
$this->createMock(Logger::class),
|
||||
$webhookServiceMock
|
||||
);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\WebhookBundle\Tests\Unit\Notificator;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use Mautic\WebhookBundle\Entity\Webhook;
|
||||
use Mautic\WebhookBundle\Event\WebhookNotificationEvent;
|
||||
use Mautic\WebhookBundle\Notificator\WebhookKillNotificator;
|
||||
use Mautic\WebhookBundle\Notificator\WebhookNotificationSender;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final class WebhookKillNotificatorTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject&TranslatorInterface
|
||||
*/
|
||||
private MockObject $translatorMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&NotificationModel
|
||||
*/
|
||||
private MockObject $notificationModelMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&EntityManager
|
||||
*/
|
||||
private MockObject $entityManagerMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&MailHelper
|
||||
*/
|
||||
private MockObject $mailHelperMock;
|
||||
|
||||
/**
|
||||
* @var MockObject&Webhook
|
||||
*/
|
||||
private MockObject $webhook;
|
||||
|
||||
/**
|
||||
* @var MockObject&CoreParametersHelper
|
||||
*/
|
||||
private MockObject $coreParamHelperMock;
|
||||
|
||||
private WebhookKillNotificator $webhookKillNotificator;
|
||||
|
||||
private string $subject = 'subject';
|
||||
|
||||
private string $reason = 'reason';
|
||||
|
||||
private string $webhookName = 'Webhook name';
|
||||
|
||||
private string $generatedRoute = 'generatedRoute';
|
||||
|
||||
private string $details = 'details';
|
||||
|
||||
private string $createdBy = 'createdBy';
|
||||
|
||||
private MockObject&User $owner;
|
||||
|
||||
private string $ownerEmail = 'toEmail';
|
||||
|
||||
private ?string $modifiedBy = null;
|
||||
|
||||
/**
|
||||
* @var MockObject|UserRepository
|
||||
*/
|
||||
private $userRepositoryMock;
|
||||
|
||||
private WebhookNotificationSender $webhookNotificationSender;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->translatorMock = $this->createMock(TranslatorInterface::class);
|
||||
$this->notificationModelMock = $this->createMock(NotificationModel::class);
|
||||
$this->entityManagerMock = $this->createMock(EntityManager::class);
|
||||
$this->mailHelperMock = $this->createMock(MailHelper::class);
|
||||
$this->coreParamHelperMock = $this->createMock(CoreParametersHelper::class);
|
||||
$this->webhook = $this->createMock(Webhook::class);
|
||||
$this->userRepositoryMock = $this->createMock(UserRepository::class);
|
||||
$twig = $this->createMock(Environment::class);
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
|
||||
$webhookNotificationEventMock = $this->createMock(WebhookNotificationEvent::class);
|
||||
$webhookNotificationEventMock->method('canSend')->willReturn(true);
|
||||
|
||||
$twig->expects(self::once())
|
||||
->method('render')
|
||||
->willReturn($this->details);
|
||||
|
||||
$this->eventDispatcher->method('dispatch')
|
||||
->willReturn(
|
||||
$webhookNotificationEventMock
|
||||
);
|
||||
$this->webhookNotificationSender =new WebhookNotificationSender(
|
||||
$twig,
|
||||
$this->notificationModelMock,
|
||||
$this->entityManagerMock,
|
||||
$this->mailHelperMock,
|
||||
$this->coreParamHelperMock,
|
||||
$this->userRepositoryMock,
|
||||
$this->eventDispatcher
|
||||
);
|
||||
}
|
||||
|
||||
public function testSendToOwner(): void
|
||||
{
|
||||
$this->mockCommonMethods(1);
|
||||
$this->webhook
|
||||
->expects($this->once())
|
||||
->method('getCreatedBy')
|
||||
->willReturn($this->createdBy);
|
||||
|
||||
$this->webhook
|
||||
->expects($this->once())
|
||||
->method('getModifiedBy')
|
||||
->willReturn($this->modifiedBy);
|
||||
|
||||
$this->entityManagerMock
|
||||
->expects($this->once())
|
||||
->method('getReference')
|
||||
->with(User::class, $this->createdBy)
|
||||
->willReturn($this->owner);
|
||||
|
||||
$this->notificationModelMock
|
||||
->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
$this->details,
|
||||
'error',
|
||||
false,
|
||||
$this->subject,
|
||||
null,
|
||||
false,
|
||||
$this->owner
|
||||
);
|
||||
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setTo')
|
||||
->with([$this->ownerEmail]);
|
||||
|
||||
$this->webhookKillNotificator->send($this->webhook, $this->reason);
|
||||
}
|
||||
|
||||
public function testSendToModifier(): void
|
||||
{
|
||||
$this->ownerEmail = 'ownerEmail';
|
||||
$this->modifiedBy = 'modifiedBy';
|
||||
$modifier = $this->createMock(User::class);
|
||||
$modifierEmail = 'modifierEmail';
|
||||
|
||||
$this->mockCommonMethods(1);
|
||||
$this->webhook
|
||||
->expects($this->exactly(2))
|
||||
->method('getCreatedBy')
|
||||
->willReturn($this->createdBy);
|
||||
$this->webhook
|
||||
->expects($this->exactly(3))
|
||||
->method('getModifiedBy')
|
||||
->willReturn($this->modifiedBy);
|
||||
$matcher = $this->exactly(2);
|
||||
|
||||
$this->entityManagerMock->expects($matcher)
|
||||
->method('getReference')->willReturnCallback(function (string $entityClass, string|int $entityId) use ($matcher, $modifier) {
|
||||
$this->assertSame(User::class, $entityClass);
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->createdBy, $entityId);
|
||||
|
||||
return $this->owner;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->modifiedBy, $entityId);
|
||||
|
||||
return $modifier;
|
||||
}
|
||||
});
|
||||
|
||||
$this->notificationModelMock
|
||||
->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
$this->details,
|
||||
'error',
|
||||
false,
|
||||
$this->subject,
|
||||
null,
|
||||
false,
|
||||
$modifier
|
||||
);
|
||||
|
||||
$modifier
|
||||
->expects($this->once())
|
||||
->method('getEmail')
|
||||
->willReturn($modifierEmail);
|
||||
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setTo')
|
||||
->with([$modifierEmail]);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setCc')
|
||||
->with([$this->ownerEmail], null);
|
||||
|
||||
$this->webhookKillNotificator->send($this->webhook, $this->reason);
|
||||
}
|
||||
|
||||
private function mockCommonMethods(int $sentToAuthor, ?string $emailToSend = null): void
|
||||
{
|
||||
$this->coreParamHelperMock->expects($this->any())
|
||||
->method('get')
|
||||
->willReturnOnConsecutiveCalls('from_name', $sentToAuthor, $emailToSend);
|
||||
|
||||
$this->webhookKillNotificator = new WebhookKillNotificator(
|
||||
$this->webhookNotificationSender,
|
||||
$this->translatorMock
|
||||
);
|
||||
$this->owner = $this->createMock(User::class);
|
||||
|
||||
$htmlUrl = '<a href="'.$this->generatedRoute.'" data-toggle="ajax">'.$this->webhookName.'</a>';
|
||||
$matcher = $this->exactly(2);
|
||||
$this->translatorMock->expects($matcher)
|
||||
->method('trans')->willReturnCallback(function (...$parameters) use ($matcher, $htmlUrl) {
|
||||
if (1 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.webhook.stopped', $parameters[0]);
|
||||
|
||||
return $this->subject;
|
||||
}
|
||||
if (2 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame($this->reason, $parameters[0]);
|
||||
|
||||
return $this->reason;
|
||||
}
|
||||
if (3 === $matcher->numberOfInvocations()) {
|
||||
$this->assertSame('mautic.webhook.stopped.details', $parameters[0]);
|
||||
$this->assertSame(['%reason%' => $this->reason, '%webhook%' => $htmlUrl], $parameters[1]);
|
||||
|
||||
return $this->details;
|
||||
}
|
||||
});
|
||||
|
||||
$this->webhook->expects($this->once())
|
||||
->method('getUnHealthySince')
|
||||
->willReturn(new \DateTimeImmutable());
|
||||
|
||||
if ($sentToAuthor) {
|
||||
$this->owner
|
||||
->expects($this->once())
|
||||
->method('getEmail')
|
||||
->willReturn($this->ownerEmail);
|
||||
}
|
||||
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setSubject')
|
||||
->with($this->subject);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setBody')
|
||||
->with($this->details);
|
||||
}
|
||||
|
||||
public function testSendToAuthorWithCC(): void
|
||||
{
|
||||
$subject = 'subject';
|
||||
$reason = 'reason';
|
||||
$webhookName = 'Webhook name';
|
||||
$generatedRoute = 'generatedRoute';
|
||||
$details = 'details';
|
||||
$createdById = 1;
|
||||
$owner = $this->createMock(User::class);
|
||||
$ownerEmail = 'owner-email@email.com';
|
||||
$modifiedById = 2;
|
||||
$modifiedBy = $this->createMock(User::class);
|
||||
$modifiedByEmail = 'modified-by@email.com';
|
||||
$htmlUrl = '<a href="'.$generatedRoute.'" data-toggle="ajax">'.$webhookName.'</a>';
|
||||
|
||||
$this->translatorMock
|
||||
->method('trans')
|
||||
->willReturnMap([
|
||||
['mautic.webhook.stopped', [], null, null, $subject],
|
||||
[$reason, [], null, null, $reason],
|
||||
[
|
||||
'mautic.webhook.stopped.details',
|
||||
[
|
||||
'%reason%' => $reason,
|
||||
'%webhook%' => $htmlUrl,
|
||||
],
|
||||
null,
|
||||
null,
|
||||
$details,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->webhook->expects($this->once())
|
||||
->method('getUnHealthySince')
|
||||
->willReturn(new \DateTimeImmutable());
|
||||
|
||||
$this->webhook
|
||||
->expects($this->exactly(2))
|
||||
->method('getCreatedBy')
|
||||
->willReturn($createdById);
|
||||
$this->webhook
|
||||
->expects($this->exactly(3))
|
||||
->method('getModifiedBy')
|
||||
->willReturn($modifiedById);
|
||||
|
||||
$this->entityManagerMock
|
||||
->method('getReference')
|
||||
->willReturnMap([
|
||||
[User::class, $createdById, $owner],
|
||||
[User::class, $modifiedById, $modifiedBy],
|
||||
]);
|
||||
|
||||
$this->notificationModelMock
|
||||
->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
$details,
|
||||
'error',
|
||||
false,
|
||||
$subject,
|
||||
null,
|
||||
null,
|
||||
$modifiedBy
|
||||
);
|
||||
|
||||
$modifiedBy->expects(self::atLeastOnce())->method('getEmail')->willReturn($modifiedByEmail);
|
||||
$owner->expects(self::atLeastOnce())->method('getEmail')->willReturn($ownerEmail);
|
||||
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setTo')
|
||||
->with([$modifiedByEmail], null);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setCc')
|
||||
->with([$ownerEmail], null);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setSubject')
|
||||
->with($subject);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setBody')
|
||||
->with($details);
|
||||
|
||||
$this->coreParamHelperMock->expects(self::atLeastOnce())
|
||||
->method('get')
|
||||
->willReturnMap([
|
||||
['webhook_send_notification_to_author', 1, true],
|
||||
['mailer_from_name', null, 'from_name'],
|
||||
]);
|
||||
|
||||
$webhookKillNotificator = new WebhookKillNotificator(
|
||||
$this->webhookNotificationSender,
|
||||
$this->translatorMock
|
||||
);
|
||||
$webhookKillNotificator->send($this->webhook, $reason);
|
||||
}
|
||||
|
||||
public function testSendToWebHookNotificationEmail(): void
|
||||
{
|
||||
$subject = 'subject';
|
||||
$reason = 'reason';
|
||||
$webhookName = 'Webhook name';
|
||||
$generatedRoute = 'generatedRoute';
|
||||
$details = 'details';
|
||||
$createdById = 1;
|
||||
$owner = $this->createMock(User::class);
|
||||
$ownerEmail = 'owner@email.com';
|
||||
$modifiedBy = null;
|
||||
$htmlUrl = '<a href="'.$generatedRoute.'" data-toggle="ajax">'.$webhookName.'</a>';
|
||||
|
||||
$this->translatorMock
|
||||
->method('trans')
|
||||
->willReturnMap([
|
||||
['mautic.webhook.stopped', [], null, null, $subject],
|
||||
[$reason, [], null, null, $reason],
|
||||
[
|
||||
'mautic.webhook.stopped.details',
|
||||
[
|
||||
'%reason%' => $reason,
|
||||
'%webhook%' => $htmlUrl,
|
||||
],
|
||||
null,
|
||||
null,
|
||||
$details,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->webhook->expects($this->once())
|
||||
->method('getUnHealthySince')
|
||||
->willReturn(new \DateTimeImmutable());
|
||||
|
||||
$this->webhook
|
||||
->expects($this->once())
|
||||
->method('getCreatedBy')
|
||||
->willReturn($createdById);
|
||||
$this->webhook
|
||||
->expects($this->once())
|
||||
->method('getModifiedBy')
|
||||
->willReturn($modifiedBy);
|
||||
|
||||
$this->entityManagerMock
|
||||
->expects($this->once())
|
||||
->method('getReference')
|
||||
->with(User::class, $createdById)
|
||||
->willReturn($owner);
|
||||
|
||||
$this->notificationModelMock
|
||||
->expects($this->once())
|
||||
->method('addNotification')
|
||||
->with(
|
||||
$details,
|
||||
'error',
|
||||
false,
|
||||
$subject,
|
||||
null,
|
||||
null,
|
||||
$owner
|
||||
);
|
||||
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setTo')
|
||||
->with([$ownerEmail], null);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setSubject')
|
||||
->with($subject);
|
||||
$this->mailHelperMock
|
||||
->expects($this->once())
|
||||
->method('setBody')
|
||||
->with($details);
|
||||
|
||||
$this->coreParamHelperMock->expects(self::atLeastOnce())
|
||||
->method('get')
|
||||
->willReturnMap([
|
||||
['webhook_send_notification_to_author', 1, false],
|
||||
['webhook_notification_email_addresses', null, $ownerEmail],
|
||||
['mailer_from_name', null, 'from_name'],
|
||||
]);
|
||||
|
||||
$webhookKillNotificator = new WebhookKillNotificator(
|
||||
$this->webhookNotificationSender,
|
||||
$this->translatorMock
|
||||
);
|
||||
$webhookKillNotificator->send($this->webhook, $reason);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user