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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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