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,114 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Mautic\NotificationBundle\Form\Type\MobileNotificationDetailsType;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
class MobileNotificationDetailsTypeTest extends TypeTestCase
{
/**
* @var MockObject&Integration
*/
private MockObject $integrationSettings;
/**
* @return array<FormExtensionInterface>
*/
protected function getExtensions(): array
{
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->addMethodMapping('loadValidatorMetadata');
$this->integrationSettings = $this->createMock(Integration::class);
// @phpstan-ignore-next-line
$integration = $this->createMock(AbstractIntegration::class);
$integration->method('getIntegrationSettings')
->willReturn($this->integrationSettings);
$integrationHelper = $this->createMock(IntegrationHelper::class);
$integrationHelper->method('getIntegrationObject')
->with('OneSignal')
->willReturn($integration);
return [
new ValidatorExtension($validatorBuilder->getValidator()),
new PreloadedExtension([
new MobileNotificationDetailsType($integrationHelper),
], []),
];
}
public function testNoPlatformsSelected(): void
{
$this->integrationSettings->method('getFeatureSettings')
->willReturn([]);
$form = $this->factory->create(MobileNotificationDetailsType::class);
$view = $form->createView();
// test only field is "additional_data"
self::assertCount(1, $view->children);
self::assertArrayHasKey('additional_data', $view->children);
}
/**
* @param array<int, string> $platforms
* @param array<int, string> $settings
*/
#[\PHPUnit\Framework\Attributes\DataProvider('platformProvider')]
public function testPlatformSelected(array $platforms, array $settings): void
{
$this->integrationSettings->method('getFeatureSettings')
->willReturn(['platforms' => $platforms]);
$form = $this->factory->create(MobileNotificationDetailsType::class);
$view = $form->createView();
self::assertCount(1 + count($settings), $view->children);
self::assertArrayHasKey('additional_data', $view->children);
foreach ($settings as $settingField) {
self::assertArrayHasKey($settingField, $view->children);
}
}
public static function platformProvider(): \Generator
{
$iosSettings = [
'ios_subtitle',
'ios_sound',
'ios_badges',
'ios_badgeCount',
'ios_contentAvailable',
'ios_media',
'ios_mutableContent',
];
$androidSettings = [
'android_sound',
'android_small_icon',
'android_large_icon',
'android_big_picture',
'android_led_color',
'android_accent_color',
'android_group_key',
'android_lockscreen_visibility',
];
yield 'ios' => [['ios'], $iosSettings];
yield 'android' => [['android'], $androidSettings];
yield 'both' => [['android', 'ios'], array_merge($androidSettings, $iosSettings)];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\DBAL\Connection;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Mautic\NotificationBundle\Form\Type\MobileNotificationSendType;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
final class MobileNotificationSendTypeTest extends TypeTestCase
{
private RouterInterface $router;
private TranslatorInterface $translator;
private Connection $connection;
/**
* @var ModelFactory<object>&MockObject
*/
private ModelFactory $modelFactory;
protected function setUp(): void
{
$this->router = $this->createMock(RouterInterface::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->modelFactory = $this->createMock(ModelFactory::class);
$this->connection = $this->createMock(Connection::class);
parent::setup();
}
/**
* @return array<mixed>
*/
protected function getExtensions()
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
new MobileNotificationSendType($this->router),
new EntityLookupType($this->modelFactory, $this->translator, $this->connection, $this->router),
], []),
];
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(MobileNotificationSendType::class);
$expected = [
'notification' => '1',
];
$form->submit([
'notification' => '1',
]);
// This check ensures there are no transformation failures
$this->assertTrue($form->isSynchronized());
// check that $model was modified as expected when the form was submitted
$this->assertEquals($expected, $form->getData());
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\DBAL\Connection;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Form\Type\EntityLookupType;
use Mautic\NotificationBundle\Form\Type\NotificationSendType;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
final class NotificationSendTypeTest extends TypeTestCase
{
private RouterInterface $router;
private TranslatorInterface $translator;
private Connection $connection;
/**
* @var ModelFactory<object>&MockObject
*/
private ModelFactory $modelFactory;
protected function setUp(): void
{
$this->router = $this->createMock(RouterInterface::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->modelFactory = $this->createMock(ModelFactory::class);
$this->connection = $this->createMock(Connection::class);
parent::setup();
}
/**
* @return array<mixed>
*/
protected function getExtensions()
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
new NotificationSendType($this->router),
new EntityLookupType($this->modelFactory, $this->translator, $this->connection, $this->router),
], []),
];
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(NotificationSendType::class);
$expected = [
'notification' => '1',
];
$form->submit([
'notification' => '1',
]);
// This check ensures there are no transformation failures
$this->assertTrue($form->isSynchronized());
// check that $model was modified as expected when the form was submitted
$this->assertEquals($expected, $form->getData());
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Form\Type\NotificationType;
use PHPUnit\Framework\Assert;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormErrorIterator;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
class NotificationTypeTest extends TypeTestCase
{
/**
* @return array<FormExtensionInterface>
*/
protected function getExtensions(): array
{
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->addMethodMapping('loadValidatorMetadata');
return [
new ValidatorExtension($validatorBuilder->getValidator()),
new PreloadedExtension([
new CategoryListType(
$this->createMock(EntityManager::class),
$this->createMock(TranslatorInterface::class),
$this->createMock(CategoryModel::class),
$this->createMock(RouterInterface::class),
),
], []),
];
}
public function testSubmitInvalidData(): void
{
$form = $this->factory->create(NotificationType::class);
$expected = new Notification();
$expected->setLanguage('en');
$expected->setUtmTags([
'utmSource' => null,
'utmMedium' => null,
'utmCampaign' => null,
'utmContent' => null,
]);
$expected->setIsPublished(false);
$form->submit([
'language' => 'en',
]);
Assert::assertTrue($form->isSynchronized());
$formData = $form->getData();
\assert($formData instanceof Notification);
$expected->setChanges($formData->getChanges());
Assert::assertEquals($expected, $formData);
Assert::assertFalse($form->isValid());
$view = $form->createView();
$invalidFields = ['name', 'heading', 'message'];
$errorCount = 0;
foreach ($view->children as $fieldName => $child) {
$errors = $view->children[$fieldName]->vars['errors'];
\assert($errors instanceof FormErrorIterator);
if (in_array($fieldName, $invalidFields, true)) {
++$errorCount;
self::assertCount(1, $errors);
continue;
}
self::assertCount(0, $errors);
}
self::assertCount($errorCount, $invalidFields);
self::assertCount(0, $view->vars['errors']);
}
public function testSubmitValidData(): void
{
$form = $this->factory->create(NotificationType::class);
$expected = new Notification();
$expected->setLanguage('en');
$expected->setName('The name');
$expected->setHeading('The heading');
$expected->setMessage('The message');
$expected->setUtmTags([
'utmSource' => null,
'utmMedium' => null,
'utmCampaign' => null,
'utmContent' => null,
]);
$expected->setIsPublished(false);
$form->submit([
'name' => 'The name',
'heading' => 'The heading',
'message' => 'The message',
'language' => 'en',
]);
Assert::assertTrue($form->isSynchronized());
$formData = $form->getData();
\assert($formData instanceof Notification);
$expected->setChanges($formData->getChanges());
Assert::assertEquals($expected, $formData);
Assert::assertTrue($form->isValid());
$view = $form->createView();
foreach ($view->children as $fieldName => $child) {
$errors = $view->children[$fieldName]->vars['errors'];
\assert($errors instanceof FormErrorIterator);
self::assertCount(0, $errors);
}
self::assertCount(0, $view->vars['errors']);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class MobileNotificationControllerTest extends MauticMysqlTestCase
{
/**
* Smoke test to ensure the '/s/mobile_notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/mobile_notifications');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
public function testCreateRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/mobile_notifications/new');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\Entity\Stat;
use Symfony\Component\HttpFoundation\Request;
final class MobileNotificationTranslationFunctionalTest extends MauticMysqlTestCase
{
public function testNotificationCanBeCreatedWithTranslationParent(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/new');
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[name]'] = 'Child Notification';
$form['mobile_notification[message]'] = 'Child Notification message';
$form['mobile_notification[heading]'] = 'Child Notification';
$form['mobile_notification[translationParentSelector]'] = (string) $parentNotification->getId();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert
$childNotification = $this->em->getRepository(Notification::class)->findOneBy(['name' => 'Child Notification']);
$this->assertInstanceOf(Notification::class, $childNotification);
$this->assertInstanceOf(Notification::class, $childNotification->getTranslationParent());
$this->assertSame($parentNotification->getId(), $childNotification->getTranslationParent()->getId());
}
public function testNotificationCannotBeItsOwnTranslationParent(): void
{
// Arrange
$notification = $this->createAndPersistNotification('Test Notification', 'Test Notification message');
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$notification->getId());
$this->assertResponseIsSuccessful();
// Assert
$options = $crawler->filter('#mobile_notification_translationParentSelector option');
$this->assertCount(2, $options);
$this->assertSame('Choose a translated item...', $options->eq(0)->text());
$this->assertSame('Create new...', $options->eq(1)->text());
// Ensure the Notification itself is not in the dropdown
$optionValues = $options->each(fn ($node) => $node->attr('value'));
$this->assertNotContains((string) $notification->getId(), $optionValues);
}
public function testNotificationWithTranslationParentCanBeEdited(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message');
$childNotification->setTranslationParent($parentNotification);
$newParentNotification = $this->createAndPersistNotification('New Parent Notification', 'New Parent Notification message');
$this->em->flush();
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
// Assert original parent is selected
$this->assertSame(
(string) $parentNotification->getId(),
$crawler->filter('#mobile_notification_translationParentSelector option[selected]')->attr('value')
);
// Change parent
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[translationParentSelector]'] = (string) $newParentNotification->getId();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert parent updated
$this->em->refresh($childNotification);
$this->assertInstanceOf(Notification::class, $childNotification->getTranslationParent());
$this->assertSame($newParentNotification->getId(), $childNotification->getTranslationParent()->getId());
}
public function testTranslationParentCanBeRemovedFromNotification(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message');
$childNotification->setTranslationParent($parentNotification);
$this->em->flush();
// Act
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/edit/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
$form = $crawler->selectButton('Save')->form();
$form['mobile_notification[translationParentSelector]'] = '';
$this->client->submit($form);
$this->assertResponseIsSuccessful();
// Assert
$this->em->refresh($childNotification);
$this->assertNull($childNotification->getTranslationParent());
}
public function testTranslationsAreDisplayedOnViewPage(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message', 'en');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message', 'fr');
$childNotification->setTranslationParent($parentNotification);
$parentNotification->addTranslationChild($childNotification);
$this->em->flush();
// Act & Assert - Parent view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/view/'.$parentNotification->getId());
$this->assertResponseIsSuccessful();
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
$this->client->click($crawler->selectLink('Translations')->link());
$this->assertSelectorTextContains('#translation-container', 'Child Notification');
// Act & Assert - Child view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications/view/'.$childNotification->getId());
$this->assertResponseIsSuccessful();
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
$this->client->click($crawler->selectLink('Translations')->link());
$this->assertSelectorTextContains('#translation-container', 'Parent Notification');
}
public function testListPageWithSentStats(): void
{
// Arrange
$parentNotification = $this->createAndPersistNotification('Parent Notification', 'Parent Notification message', 'en');
$childNotification = $this->createAndPersistNotification('Child Notification', 'Child Notification message', 'fr');
$childNotification->setTranslationParent($parentNotification);
$parentNotification->addTranslationChild($childNotification);
$this->em->flush();
// Create a stat
$this->createStatEntry($parentNotification, $this->createContact('user', 'one'));
$this->createStatEntry($childNotification, $this->createContact('user', 'two'));
// Act & Assert - list view
$crawler = $this->client->request(Request::METHOD_GET, '/s/mobile_notifications');
$this->assertResponseIsSuccessful();
$this->assertCount(2, $crawler->filterXPath("//td[contains(@class, 'col-stats')]"));
}
private function createANotification(string $name, string $message, bool $isPublished = true, string $locale = 'en'): Notification
{
$notification = new Notification();
$notification->setName($name);
$notification->setMessage($message);
$notification->setHeading($name);
$notification->setLanguage($locale);
$notification->setIsPublished($isPublished);
$notification->setMobile(true);
return $notification;
}
private function createAndPersistNotification(string $name, string $message, string $locale = 'en'): Notification
{
$notification = $this->createANotification($name, $message, true, $locale);
$this->em->persist($notification);
$this->em->flush();
return $notification;
}
public function createStatEntry(Notification $notification, Lead $lead): void
{
$stat = new Stat();
$stat->setDateSent(new \DateTime());
$stat->setLead($lead);
$stat->setNotification($notification);
$stat->setSource(null);
$stat->setSourceId(null);
$this->em->persist($stat);
$this->em->flush();
}
private function createContact(string $firstname, string $lastname): Lead
{
$contact = new Lead();
$contact->setFirstname($firstname);
$contact->setLastname($lastname);
$this->em->persist($contact);
$this->em->flush();
return $contact;
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\NotificationBundle\Tests\NotificationTrait;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class NotificationControllerTest extends MauticMysqlTestCase
{
use NotificationTrait;
/**
* @var string
*/
private const REST_API_ID = 'restApiID';
/**
* @var string
*/
private const API_ID = 'apiID';
/**
* Smoke test to ensure the '/s/notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/notifications');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
/**
* Smoke test to ensure the '/s/notifications/new' route loads.
*/
public function testNewRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/s/notifications/new');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
public function testNewWebNotificationValidSubmit(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/new');
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
$form->setValues([
'notification[name]' => 'Some Name',
'notification[heading]' => 'Some Heading',
'notification[message]' => 'some message',
]);
$crawler = $this->client->submit($form);
Assert::assertStringContainsString('Some Name has been created!', $crawler->text());
}
public function testNewWebNotificationValidationErrors(): void
{
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/new');
$this->assertValidationErrors($crawler);
}
public function testEditWebNotificationValidationErrors(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request(Request::METHOD_GET, '/s/notifications/edit/'.$notification->getid());
$this->assertValidationErrors($crawler);
}
private function assertValidationErrors(Crawler $crawler): void
{
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
// test blank errors
$form = $formCrawler->form();
$form->setValues([
'notification[name]' => '',
'notification[heading]' => '',
'notification[message]' => '',
]);
$crawler = $this->client->submit($form);
$formCrawler = $crawler->filter('form[name=notification]');
$this->assertCount(1, $formCrawler);
Assert::assertMatchesRegularExpression('/A name is required\./', $formCrawler->text());
Assert::assertMatchesRegularExpression('/A heading is required\./', $formCrawler->text());
Assert::assertMatchesRegularExpression('/A message is required\./', $formCrawler->text());
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class PopupControllerTest extends MauticMysqlTestCase
{
/**
* Smoke test to ensure the '/s/notifications' route loads.
*/
public function testIndexRouteSuccessfullyLoads(): void
{
$this->client->request(Request::METHOD_GET, '/notification');
$response = $this->client->getResponse();
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,617 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Functional\EventListener;
use GuzzleHttp\Psr7\Response;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event as CampaignEvent;
use Mautic\CampaignBundle\Entity\Lead as CampaignLead;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\NotificationBundle\Api\AbstractNotificationApi;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\NotificationBundle\EventListener\CampaignSubscriber;
use Mautic\NotificationBundle\Tests\NotificationTrait;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\RequestInterface;
class CampaignSubscriberTest extends MauticMysqlTestCase
{
use NotificationTrait;
/**
* @var string
*/
private const REST_API_ID = 'restApiID';
/**
* @var string
*/
private const API_ID = 'apiID';
/**
* @var string
*/
private const ONESIGNAL_API_BASE_URL = 'https://onesignal.com/api/v1/notifications';
public function testLeadNotContactable(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$leadThree = $this->createLeadInCampaign($campaign, ['web-3a', 'web-3b']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->createDoNotContact($leadOne, $notification);
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-2', 'web-3a', 'web-3b'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Contact is not contactable on the Web Notification channel.');
$this->assertEventLogPassed($event, $leadTwo);
$this->assertEventLogPassed($event, $leadThree);
}
public function testNotificationMissing(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$event->setProperties([]);
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The specified Web Notification entity does not exist.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testNotificationUnpublished(): void
{
$notification = $this->createNotification($this->em);
$notification->setIsPublished(false);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The specified Web Notification is unpublished.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testNotificationWithEmptyPushIds(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, []);
$leadTwo = $this->createLeadInCampaign($campaign, []);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'The contact has not subscribed to the Web Notification channel.';
$this->assertEventLogFailed($event, $leadOne, $reason);
$this->assertEventLogFailed($event, $leadTwo, $reason);
}
public function testWebNotificationsAreSent(): void
{
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a', 'web-2b', 'web-3'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testMobileNotificationsAreSent(): void
{
$notification = $this->createNotification($this->em);
$notification->setMobile(true);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadTwo = $this->createLeadInCampaign($campaign, ['web-1']);
$leadThree = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$leadFour = $this->createLeadInCampaign($campaign, ['mobile-3a', 'mobile-3b'], true);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(
['mobile-1', 'mobile-2a', 'mobile-2b', 'mobile-3a', 'mobile-3b'],
$notification
),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testWebAndMobileNotificationsAreSent(): void
{
$webNotification = $this->createNotification($this->em);
$webNotification->setHeading('Web heading 1');
$webNotification->setMessage('Web message 1');
$mobileNotification = $this->createNotification($this->em);
$mobileNotification->setHeading('Mobile heading 1');
$mobileNotification->setMessage('Mobile message 1');
$mobileNotification->setMobile(true);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$leadFour = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$webEvent = $this->createCampaignEvent($campaign, $webNotification, 'notification.send_notification');
$mobileEvent = $this->createCampaignEvent($campaign, $mobileNotification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a', 'web-2b'], $webNotification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['mobile-1', 'mobile-2a', 'mobile-2b'], $mobileNotification),
'POST',
self::ONESIGNAL_API_BASE_URL,
500,
'Internal server error'
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$reason = 'Internal server error (500)';
$this->assertEventLogPassed($webEvent, $leadOne);
$this->assertEventLogFailed($mobileEvent, $leadTwo, $reason, true);
$this->assertEventLogFailed($mobileEvent, $leadThree, $reason, true);
$this->assertEventLogPassed($webEvent, $leadFour);
}
public function testNotificationsWithToken(): void
{
$notification = $this->createNotification($this->em);
$notification->setMessage('Message {contactfield=email}');
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadOne->setEmail('one@domain.tld');
$leadTwo = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadTwo->setEmail('two@domain.tld');
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-1'],
'contents' => ['en' => 'Message '.$leadOne->getEmail()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL,
400,
'Bad Request'
));
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-2a', 'web-2b'],
'contents' => ['en' => 'Message '.$leadTwo->getEmail()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL,
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Bad Request (400)', true);
$this->assertEventLogPassed($event, $leadTwo);
}
public function testWebNotificationsWithUrlAndButtons(): void
{
$notification = $this->createNotification($this->em);
$notification->setUrl('https://some-url.tld');
$notification->setButton('Some button');
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, []);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3a', 'web-3b']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->createDoNotContact($leadOne, $notification);
$this->em->flush();
$this->em->clear();
$urlThree = $this->convertToTrackedUrl($notification, $leadThree);
$urlFour = $this->convertToTrackedUrl($notification, $leadFour);
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-2'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'url' => $urlThree,
'web_buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
'url' => $urlThree,
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['web-3a', 'web-3b'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'url' => $urlFour,
'web_buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
'url' => $urlFour,
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->triggerCampaigns();
$this->assertEventLogFailed($event, $leadOne, 'Contact is not contactable on the Web Notification channel.');
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
}
public function testMobileNotificationsWithButtonsAndSettings(): void
{
$notification = $this->createNotification($this->em);
$notification->setMobile(true);
$notification->setButton('Some button');
$notification->setMobileSettings([
'ios_subtitle' => 'iOS Subtitle',
'android_led_color' => 'FF00DD',
]);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-2a', 'mobile-2b'], true);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_mobile_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
[
'include_player_ids' => ['mobile-1', 'mobile-2a', 'mobile-2b'],
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'subtitle' => ['en' => $notification->getMobileSettings()['ios_subtitle']],
'android_led_color' => 'FF'.$notification->getMobileSettings()['android_led_color'],
'buttons' => [
[
'id' => $notification->getHeading(),
'text' => $notification->getButton(),
],
],
'app_id' => self::API_ID,
],
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogPassed($event, $leadTwo);
}
public function testNotificationsSentInBatches(): void
{
$subscriber = new class(static::getContainer()->get('mautic.helper.integration'), static::getContainer()->get('mautic.notification.model.notification'), static::getContainer()->get('mautic.notification.api'), static::getContainer()->get('event_dispatcher'), static::getContainer()->get('mautic.lead.model.dnc'), static::getContainer()->get('translator')) extends CampaignSubscriber {
protected const MAX_PLAYER_IDS_PER_REQUEST = 2;
};
static::getContainer()->set('mautic.notification.campaignbundle.subscriber', $subscriber);
$notification = $this->createNotification($this->em);
$this->em->flush();
$campaign = $this->createCampaign($this->em);
$leadOne = $this->createLeadInCampaign($campaign, ['web-1']);
$leadTwo = $this->createLeadInCampaign($campaign, ['mobile-1'], true);
$leadThree = $this->createLeadInCampaign($campaign, ['web-2a', 'web-2b']);
$leadFour = $this->createLeadInCampaign($campaign, ['web-3']);
$leadFive = $this->createLeadInCampaign($campaign, ['web-4']);
$event = $this->createCampaignEvent($campaign, $notification, 'notification.send_notification');
$this->em->flush();
$this->em->clear();
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-1', 'web-2a'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-2b', 'web-3'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->responseDataAssertion(
$this->getExpectedResponsePushIds(['web-4'], $notification),
'POST',
self::ONESIGNAL_API_BASE_URL
));
$this->transportMock->append($this->noMoreRequestAssertion());
$this->triggerCampaigns();
$this->assertEventLogPassed($event, $leadOne);
$this->assertEventLogFailed($event, $leadTwo, 'The contact has not subscribed to the Web Notification channel.');
$this->assertEventLogPassed($event, $leadThree);
$this->assertEventLogPassed($event, $leadFour);
$this->assertEventLogPassed($event, $leadFive);
}
/**
* @param string[] $pushIds
*/
private function createLeadInCampaign(Campaign $campaign, array $pushIds, bool $mobile = false): Lead
{
$lead = new Lead();
foreach ($pushIds as $pushId) {
$lead->addPushIDEntry($pushId, true, $mobile);
}
$this->em->persist($lead);
$campaignLead = new CampaignLead();
$campaignLead->setCampaign($campaign);
$campaignLead->setLead($lead);
$campaignLead->setDateAdded(new \DateTime());
$this->em->persist($campaignLead);
return $lead;
}
private function createCampaignEvent(Campaign $campaign, Notification $notification, string $type): CampaignEvent
{
$campaignEvent = new CampaignEvent();
$campaignEvent->setCampaign($campaign);
$campaignEvent->setName('Send notification');
$campaignEvent->setType($type);
$campaignEvent->setEventType('action');
$campaignEvent->setProperties(['notification' => $notification->getId()]);
$this->em->persist($campaignEvent);
return $campaignEvent;
}
private function triggerCampaigns(): void
{
$this->testSymfonyCommand('mautic:campaigns:trigger');
$this->em->clear();
}
/**
* @param mixed[] $expectedData
*/
private function responseDataAssertion(
array $expectedData,
string $expectedMethod = 'GET',
string $expectedUri = '',
int $status = 200,
?string $body = null,
): callable {
return static function (RequestInterface $request) use ($expectedData, $expectedMethod, $expectedUri, $status, $body) {
Assert::assertSame($expectedMethod, $request->getMethod());
Assert::assertSame($expectedUri, $request->getUri()->__toString());
Assert::assertSame(json_encode($expectedData), $request->getBody()->getContents());
$headers = $request->getHeaders();
unset($headers['Content-Length']);
Assert::assertSame([
'User-Agent' => ['GuzzleHttp/7'],
'Host' => ['onesignal.com'],
'Authorization' => ['Basic '.self::REST_API_ID],
'Content-Type' => ['application/json'],
], $headers);
return new Response($status, [], $body);
};
}
/**
* @param array<string> $pushIds
*
* @return array<mixed>
*/
private function getExpectedResponsePushIds(array $pushIds, Notification $notification): array
{
return array_merge(
['include_player_ids' => $pushIds],
[
'contents' => ['en' => $notification->getMessage()],
'headings' => ['en' => $notification->getHeading()],
'app_id' => self::API_ID,
]
);
}
private function noMoreRequestAssertion(): callable
{
return function () {
$this->fail('No other request was expected');
};
}
private function convertToTrackedUrl(Notification $notification, Lead $leadOne): string
{
/** @var AbstractNotificationApi $api */
$api = static::getContainer()->get('mautic.notification.api');
$clickThrough = [
'notification' => $notification->getId(),
'lead' => $leadOne->getId(),
];
return $api->convertToTrackedUrl($notification->getUrl(), $clickThrough, $notification);
}
private function assertEventLogPassed(CampaignEvent $event, Lead $leadOne): void
{
$log = $this->findEventLog($event, $leadOne);
Assert::assertFalse($log->getIsScheduled());
$metadata = $log->getMetadata();
Assert::assertIsArray($metadata);
Assert::assertArrayHasKey('status', $metadata);
Assert::assertSame('mautic.notification.timeline.status.delivered', $metadata['status']);
}
private function assertEventLogFailed(CampaignEvent $event, Lead $leadOne, ?string $reason, bool $isScheduled = false): void
{
$log = $this->findEventLog($event, $leadOne);
Assert::assertSame($isScheduled, $log->getIsScheduled());
$metadata = $log->getMetadata();
Assert::assertIsArray($metadata);
Assert::assertArrayHasKey('failed', $metadata);
Assert::assertSame(1, $metadata['failed']);
Assert::assertArrayHasKey('reason', $metadata);
Assert::assertSame($reason, $metadata['reason']);
}
private function findEventLog(CampaignEvent $event, Lead $leadOne): LeadEventLog
{
$log = $this->em->getRepository(LeadEventLog::class)->findOneBy([
'event' => $event->getId(),
'lead' => $leadOne,
'rotation' => 1,
]);
Assert::assertNotNull($log);
return $log;
}
private function createDoNotContact(Lead $lead, Notification $notification): DoNotContact
{
$doNotContact = new DoNotContact();
$doNotContact->setLead($lead);
$doNotContact->setChannel('notification');
$doNotContact->setChannelId($notification->getId());
$doNotContact->setReason(DoNotContact::UNSUBSCRIBED);
$doNotContact->setDateAdded(new \DateTime());
$this->em->persist($doNotContact);
return $doNotContact;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Handler\MockHandler;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\NotificationBundle\Entity\Notification;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Symfony\Component\DependencyInjection\ContainerInterface;
trait NotificationTrait
{
private MockHandler $transportMock;
protected function setUp(): void
{
parent::setUp();
$this->transportMock = $this->getMockHandler(static::getContainer());
$this->setupIntegration(static::getContainer(), $this->em, self::API_ID, self::REST_API_ID);
}
private function getMockHandler(ContainerInterface $container): MockHandler
{
return $container->get(MockHandler::class);
}
private function createNotification(EntityManagerInterface $em): Notification
{
$notification = new Notification();
$notification->setName('Name 1');
$notification->setHeading('Heading 1');
$notification->setMessage('Message 1');
$em->persist($notification);
return $notification;
}
private function createCampaign(EntityManagerInterface $em): Campaign
{
$campaign = new Campaign();
$campaign->setName('Notification');
$em->persist($campaign);
return $campaign;
}
private function setupIntegration(ContainerInterface $container, EntityManagerInterface $em, string $apiId, string $restApiId): void
{
/** @var AbstractIntegration $integration */
$integration = $container->get('mautic.helper.integration')
->getIntegrationObject('OneSignal');
$integrationSettings = $integration->getIntegrationSettings();
$integrationSettings->setIsPublished(true);
$integration->encryptAndSetApiKeys([
'app_id' => $apiId,
'rest_api_key' => $restApiId,
], $integrationSettings);
$em->persist($integrationSettings);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Mautic\NotificationBundle\Tests\Unit\Api;
use Mautic\NotificationBundle\Api\OneSignalApi;
use PHPUnit\Framework\TestCase;
class OneSignalApiTest extends TestCase
{
public function testAddMobileData(): void
{
$mockOneSignalApi = $this->createMock(OneSignalApi::class);
$controllerReflection = (new \ReflectionClass(OneSignalApi::class));
$method = $controllerReflection->getMethod('addMobileData');
$method->setAccessible(true);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_subtitle' => 'test']]);
$this->assertEquals(['subtitle' => ['en' => 'test']], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_sound' => 'test']]);
$this->assertEquals(['ios_sound' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_sound' => '']]);
$this->assertEquals(['ios_sound' => 'default'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_badges' => 'test']]);
$this->assertEquals(['ios_badgeType' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_badgeCount' => '5']]);
$this->assertEquals(['ios_badgeCount' => 5], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_contentAvailable' => true]]);
$this->assertEquals(['content_available' => true], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['ios_mutableContent' => true]]);
$this->assertEquals(['mutable_content' => true], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_sound' => 'test']]);
$this->assertEquals(['android_sound' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_small_icon' => 'test']]);
$this->assertEquals(['small_icon' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_large_icon' => 'test']]);
$this->assertEquals(['large_icon' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_big_picture' => 'test']]);
$this->assertEquals(['big_picture' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_led_color' => 'test']]);
$this->assertEquals(['android_led_color' => 'FFTEST'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_accent_color' => 'test']]);
$this->assertEquals(['android_accent_color' => 'FFTEST'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_group_key' => 'test']]);
$this->assertEquals(['android_group' => 'test'], $data);
$data = [];
$method->invokeArgs($mockOneSignalApi, [&$data, ['android_lockscreen_visibility' => 1]]);
$this->assertEquals(['android_visibility' => 1], $data);
$data = [];
$mobileConfig = ['additional_data' => ['list' => [
['label' => 'a', 'value' => 1],
['label' => 'b', 'value' => 2],
],
],
];
$method->invokeArgs($mockOneSignalApi, [&$data, $mobileConfig]);
$this->assertEquals(['data' => ['a' => 1, 'b' => 2]], $data);
}
}