Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -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([]));
}
}

View File

@@ -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');
}
}

View File

@@ -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,
]);
}
}