Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private FixtureHelper $campaignFixturesHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
}
|
||||
|
||||
public function testCancelScheduledCampaignEventAction(): void
|
||||
{
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
$contact = $this->campaignFixturesHelper->createContact('some@contact.email');
|
||||
$campaign = $this->campaignFixturesHelper->createCampaign('Scheduled event test');
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contact, $campaign);
|
||||
$this->campaignFixturesHelper->createCampaignWithScheduledEvent($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$commandResult = $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId()]);
|
||||
|
||||
Assert::assertStringContainsString('1 total event was scheduled', $commandResult->getDisplay());
|
||||
|
||||
$payload = [
|
||||
'action' => 'campaign:cancelScheduledCampaignEvent',
|
||||
'eventId' => $campaign->getEvents()[0]->getId(),
|
||||
'contactId' => $contact->getId(),
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
|
||||
// Ensure we'll fetch fresh data from the database and not from entity manager.
|
||||
$this->em->detach($contact);
|
||||
$this->em->detach($campaign);
|
||||
|
||||
/** @var LeadEventLogRepository $leadEventLogRepository */
|
||||
$leadEventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
|
||||
/** @var LeadEventLog $log */
|
||||
$log = $leadEventLogRepository->findOneBy(['lead' => $contact, 'campaign' => $campaign]);
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
Assert::assertFalse($log->getIsScheduled());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\UserEntityTrait;
|
||||
use Mautic\DynamicContentBundle\Entity\DynamicContent;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class CampaignApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use UserEntityTrait;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->configParams['mailer_from_name'] = 'Mautic Admin';
|
||||
$this->configParams['mailer_from_email'] = 'admin@email.com';
|
||||
$this->useCleanupRollback = false;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and persists common test entities used across multiple tests.
|
||||
*
|
||||
* @return array<string, mixed> Array containing the created entities
|
||||
*/
|
||||
private function createTestEntities(): array
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName('test');
|
||||
$segment->setAlias('test');
|
||||
$segment->setPublicName('test');
|
||||
|
||||
$email = new Email();
|
||||
$email->setName('test');
|
||||
$email->setSubject('Ahoy {contactfield=email}');
|
||||
$email->setCustomHtml('Your email is <b>{contactfield=email}</b>');
|
||||
$email->setUseOwnerAsMailer(true);
|
||||
|
||||
$dwc = new DynamicContent();
|
||||
$dwc->setName('test');
|
||||
$dwc->setSlotName('test');
|
||||
$dwc->setContent('test');
|
||||
|
||||
$company = new Company();
|
||||
$company->setName('test');
|
||||
|
||||
$contact1 = new Lead();
|
||||
$contact1->setEmail('contact@one.email');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setEmail('contact@two.email');
|
||||
$contact2->setOwner($user);
|
||||
|
||||
$member1 = new ListLead();
|
||||
$member1->setLead($contact1);
|
||||
$member1->setList($segment);
|
||||
$member1->setDateAdded(new \DateTime());
|
||||
|
||||
$member2 = new ListLead();
|
||||
$member2->setLead($contact2);
|
||||
$member2->setList($segment);
|
||||
$member2->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->persist($email);
|
||||
$this->em->persist($dwc);
|
||||
$this->em->persist($company);
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->persist($member1);
|
||||
$this->em->persist($member2);
|
||||
$this->em->flush();
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'segment' => $segment,
|
||||
'email' => $email,
|
||||
'dwc' => $dwc,
|
||||
'company' => $company,
|
||||
'contact1' => $contact1,
|
||||
'contact2' => $contact2,
|
||||
];
|
||||
}
|
||||
|
||||
public function testCreateNewCampaign(): void
|
||||
{
|
||||
$entities = $this->createTestEntities();
|
||||
$user = $entities['user'];
|
||||
$segment = $entities['segment'];
|
||||
$email = $entities['email'];
|
||||
$dwc = $entities['dwc'];
|
||||
$company = $entities['company'];
|
||||
|
||||
$payload = [
|
||||
'name' => 'test',
|
||||
'description' => 'Created via API',
|
||||
'events' => [
|
||||
[
|
||||
'id' => 'new_43', // Event ID will be replaced on /new
|
||||
'name' => 'DWC event test',
|
||||
'description' => 'API test',
|
||||
'type' => 'dwc.decision',
|
||||
'eventType' => 'decision',
|
||||
'order' => 1,
|
||||
'properties' => [
|
||||
'dwc_slot_name' => 'test',
|
||||
'dynamicContent' => $dwc->getId(),
|
||||
],
|
||||
'triggerInterval' => 0,
|
||||
'triggerIntervalUnit' => null,
|
||||
'triggerMode' => null,
|
||||
'children' => [
|
||||
'new_55', // Event ID will be replaced on /new
|
||||
],
|
||||
'parent' => null,
|
||||
'decisionPath' => null,
|
||||
],
|
||||
[
|
||||
'id' => 'new_44', // Event ID will be replaced on /new
|
||||
'name' => 'Send email',
|
||||
'description' => 'API test',
|
||||
'type' => 'email.send',
|
||||
'eventType' => 'action',
|
||||
'order' => 2,
|
||||
'properties' => [
|
||||
'email' => $email->getId(),
|
||||
'email_type' => MailHelper::EMAIL_TYPE_TRANSACTIONAL,
|
||||
],
|
||||
'triggerInterval' => 0,
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerMode' => 'interval',
|
||||
'children' => [],
|
||||
'parent' => null,
|
||||
'decisionPath' => 'yes',
|
||||
],
|
||||
[
|
||||
'id' => 'new_55', // Event ID will be replaced on /new
|
||||
'name' => 'Add to company action',
|
||||
'description' => 'API test',
|
||||
'type' => 'lead.addtocompany',
|
||||
'eventType' => 'action',
|
||||
'order' => 2,
|
||||
'properties' => [
|
||||
'company' => $company->getId(),
|
||||
],
|
||||
'triggerInterval' => 1,
|
||||
'triggerIntervalUnit' => 'd',
|
||||
'triggerMode' => 'interval',
|
||||
'children' => [],
|
||||
'parent' => 'new_43', // Event ID will be replaced on /new
|
||||
'decisionPath' => 'no',
|
||||
],
|
||||
],
|
||||
'forms' => [],
|
||||
'lists' => [
|
||||
[
|
||||
'id' => $segment->getId(),
|
||||
],
|
||||
],
|
||||
'canvasSettings' => [
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => 'new_43', // Event ID will be replaced on /new
|
||||
'positionX' => '650',
|
||||
'positionY' => '189',
|
||||
],
|
||||
[
|
||||
'id' => 'new_44', // Event ID will be replaced on /new
|
||||
'positionX' => '433',
|
||||
'positionY' => '348',
|
||||
],
|
||||
[
|
||||
'id' => 'new_55', // Event ID will be replaced on /new
|
||||
'positionX' => '750',
|
||||
'positionY' => '411',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '629',
|
||||
'positionY' => '65',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_43', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => 'new_44', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
[
|
||||
'sourceId' => 'new_43', // Event ID will be replaced on /new
|
||||
'targetId' => 'new_55', // Event ID will be replaced on /new
|
||||
'anchors' => [
|
||||
'source' => 'no',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request(Request::METHOD_POST, 'api/campaigns/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(201, $clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$campaignId = $response['campaign']['id'];
|
||||
Assert::assertGreaterThan(0, $campaignId);
|
||||
Assert::assertEquals($payload['name'], $response['campaign']['name']);
|
||||
Assert::assertEquals($payload['description'], $response['campaign']['description']);
|
||||
Assert::assertEquals($payload['events'][0]['name'], $response['campaign']['events'][0]['name']);
|
||||
Assert::assertEquals($segment->getId(), $response['campaign']['lists'][0]['id']);
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:update', ['-i' => $campaignId]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
Assert::assertStringContainsString('2 total contact(s) to be added', $commandTester->getDisplay());
|
||||
Assert::assertStringContainsString('100%', $commandTester->getDisplay());
|
||||
|
||||
$commandTester = $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => $campaignId]);
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
// 2 events were executed for each of the 2 contacts (= 4). The third event is waiting for the decision interval.
|
||||
Assert::assertStringContainsString('4 total events were executed', $commandTester->getDisplay());
|
||||
|
||||
$this->assertQueuedEmailCount(2);
|
||||
|
||||
$email1 = $this->getMailerMessagesByToAddress('contact@one.email')[0];
|
||||
|
||||
// The email is has mailer is owner ON but this contact doesn't have any owner. So it uses default FROM and Reply-To.
|
||||
Assert::assertSame('Ahoy contact@one.email', $email1->getSubject());
|
||||
Assert::assertMatchesRegularExpression('#Your email is <b>contact@one\.email<\/b><img height="1" width="1" src="https:\/\/localhost\/email\/[a-z0-9]+\.gif\?ct=[^"]+" alt="" \/>#', $email1->getHtmlBody());
|
||||
Assert::assertSame('Your email is contact@one.email', $email1->getTextBody());
|
||||
Assert::assertCount(1, $email1->getFrom());
|
||||
Assert::assertSame($this->configParams['mailer_from_name'], $email1->getFrom()[0]->getName());
|
||||
Assert::assertSame($this->configParams['mailer_from_email'], $email1->getFrom()[0]->getAddress());
|
||||
Assert::assertCount(1, $email1->getTo());
|
||||
Assert::assertSame('', $email1->getTo()[0]->getName());
|
||||
Assert::assertSame($entities['contact1']->getEmail(), $email1->getTo()[0]->getAddress());
|
||||
Assert::assertCount(1, $email1->getReplyTo());
|
||||
Assert::assertSame('', $email1->getReplyTo()[0]->getName());
|
||||
Assert::assertSame($this->configParams['mailer_from_email'], $email1->getReplyTo()[0]->getAddress());
|
||||
|
||||
$email2 = $this->getMailerMessagesByToAddress('contact@two.email')[0];
|
||||
|
||||
// This contact does have an owner so it uses FROM and Rply-to from the owner.
|
||||
Assert::assertSame('Ahoy contact@two.email', $email2->getSubject());
|
||||
Assert::assertMatchesRegularExpression('#Your email is <b>contact@two\.email<\/b><img height="1" width="1" src="https:\/\/localhost\/email\/[a-z0-9]+\.gif\?ct=[^"]*" alt="" \/>#', $email2->getHtmlBody());
|
||||
Assert::assertSame('Your email is contact@two.email', $email2->getTextBody());
|
||||
Assert::assertCount(1, $email2->getFrom());
|
||||
Assert::assertSame($user->getName(), $email2->getFrom()[0]->getName());
|
||||
Assert::assertSame($user->getEmail(), $email2->getFrom()[0]->getAddress());
|
||||
Assert::assertCount(1, $email2->getTo());
|
||||
Assert::assertSame('', $email2->getTo()[0]->getName());
|
||||
Assert::assertSame($entities['contact2']->getEmail(), $email2->getTo()[0]->getAddress());
|
||||
Assert::assertCount(1, $email2->getReplyTo());
|
||||
Assert::assertSame('', $email2->getReplyTo()[0]->getName());
|
||||
Assert::assertSame($user->getEmail(), $email2->getReplyTo()[0]->getAddress());
|
||||
|
||||
// Search for this campaign:
|
||||
$this->client->request(Request::METHOD_GET, "/api/campaigns?search=ids:{$response['campaign']['id']}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful($clientResponse->getContent());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertEquals($payload['name'], $response['campaigns'][$campaignId]['name'], $clientResponse->getContent());
|
||||
Assert::assertEquals($payload['description'], $response['campaigns'][$campaignId]['description'], $clientResponse->getContent());
|
||||
Assert::assertEquals($payload['events'][0]['name'], $response['campaigns'][$campaignId]['events'][0]['name'], $clientResponse->getContent());
|
||||
Assert::assertEquals($segment->getId(), $response['campaigns'][$campaignId]['lists'][0]['id'], $clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testExportCampaignAction(): void
|
||||
{
|
||||
$entities = $this->createTestEntities();
|
||||
$user = $entities['user'];
|
||||
$segment = $entities['segment'];
|
||||
$email = $entities['email'];
|
||||
$dwc = $entities['dwc'];
|
||||
$company = $entities['company'];
|
||||
|
||||
// Create the campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('test campaign');
|
||||
$campaign->setDescription('Test campaign for export');
|
||||
|
||||
// Create events
|
||||
$event1 = new Event();
|
||||
$event1->setName('DWC event test');
|
||||
$event1->setDescription('API test');
|
||||
$event1->setType('dwc.decision');
|
||||
$event1->setEventType('decision'); // Set the event type
|
||||
$event1->setCampaign($campaign); // Set the campaign for this event
|
||||
$event1->setTriggerWindow(null);
|
||||
|
||||
$event2 = new Event();
|
||||
$event2->setName('Send email');
|
||||
$event2->setDescription('API test');
|
||||
$event2->setType('email.send');
|
||||
$event2->setEventType('action'); // Set the event type
|
||||
$event2->setCampaign($campaign); // Set the campaign for this event
|
||||
$event2->setTriggerWindow(null);
|
||||
|
||||
// Add events to the campaign (using addEvents)
|
||||
$campaign->addEvents([
|
||||
'new_43' => $event1, // Key for event1
|
||||
'new_44' => $event2, // Key for event2
|
||||
]);
|
||||
|
||||
// Persist campaign and events
|
||||
$this->em->persist($event1);
|
||||
$this->em->persist($event2);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Export the campaign
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/99999');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseStatusCodeSame(404, (string) $clientResponse->getStatusCode());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/'.$campaign->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
// Check response status code
|
||||
$this->assertResponseStatusCodeSame(200, (string) $clientResponse->getStatusCode());
|
||||
|
||||
// Decode the response content
|
||||
$responseData = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Ensure the response contains campaign data
|
||||
$this->assertNotEmpty($responseData);
|
||||
$this->assertArrayHasKey('campaign', $responseData[0]);
|
||||
|
||||
// Since 'campaign' is an array, we'll need to check the first element
|
||||
$this->assertArrayHasKey('name', $responseData[0]['campaign'][0]); // Access the first campaign in the array
|
||||
$this->assertEquals($campaign->getName(), $responseData[0]['campaign'][0]['name']);
|
||||
$this->assertEquals($campaign->getDescription(), $responseData[0]['campaign'][0]['description']);
|
||||
|
||||
// Check if the campaign export includes the expected events
|
||||
$this->assertCount(2, $responseData[0]['campaign_event']);
|
||||
|
||||
// Ensure proper serialization of the campaign events
|
||||
foreach ($responseData[0]['campaign_event'] as $event) {
|
||||
$this->assertArrayHasKey('id', $event);
|
||||
$this->assertArrayHasKey('name', $event);
|
||||
// Additional checks for event properties if necessary
|
||||
}
|
||||
}
|
||||
|
||||
public function testExportCampaignActionAccessDenied(): void
|
||||
{
|
||||
// Create a user without export permissions
|
||||
$nonAdminUser = $this->createUserWithPermission([
|
||||
'user-name' => 'non-admin',
|
||||
'email' => 'non-admin@mautic-test.com',
|
||||
'first-name' => 'non-admin',
|
||||
'last-name' => 'non-admin',
|
||||
'role' => [
|
||||
'name' => 'perm_non_admin',
|
||||
'permissions' => [
|
||||
'campaign:campaigns' => 2,
|
||||
'campaign:export:enable' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginUser($nonAdminUser);
|
||||
|
||||
// Create and persist a campaign
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$campaign->setDescription('Test description');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Attempt to export the campaign
|
||||
$this->client->request(Request::METHOD_GET, '/api/campaigns/export/'.$campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Assert that access is denied
|
||||
$this->assertEquals(403, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportCampaignActionJson(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/campaigns/import',
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
json_encode(FixtureHelper::getPayload(), JSON_PRETTY_PRINT)
|
||||
);
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
// Debug early exit if something fails
|
||||
if (201 !== $clientResponse->getStatusCode()) {
|
||||
$this->fail('Import failed with error: '.$clientResponse->getContent());
|
||||
}
|
||||
|
||||
// Success check
|
||||
$this->assertResponseStatusCodeSame(201, 'Expected status code 201 for successful import.');
|
||||
$responseData = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertIsArray($responseData);
|
||||
$this->assertContains('Import successful: Imported campaigns are switched off by default.', $responseData);
|
||||
}
|
||||
|
||||
public function testImportCampaignActionZip(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create temporary zip file
|
||||
$zip = new \ZipArchive();
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'mautic_zip_test').'.zip';
|
||||
|
||||
if (true === $zip->open($zipPath, \ZipArchive::CREATE)) {
|
||||
$zip->addFromString('campaign.json', json_encode(FixtureHelper::getPayload(), JSON_PRETTY_PRINT));
|
||||
$zip->close();
|
||||
} else {
|
||||
$this->fail('Failed to create test ZIP file.');
|
||||
}
|
||||
|
||||
// Upload via API
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/campaigns/import',
|
||||
[],
|
||||
['file' => new \Symfony\Component\HttpFoundation\File\UploadedFile($zipPath, 'import.zip')],
|
||||
['CONTENT_TYPE' => 'multipart/form-data']
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Clean up file
|
||||
unlink($zipPath);
|
||||
|
||||
if (201 !== $response->getStatusCode()) {
|
||||
$this->fail('Import failed with error: '.$response->getContent());
|
||||
}
|
||||
|
||||
$this->assertResponseStatusCodeSame(201);
|
||||
$decoded = json_decode($response->getContent(), true);
|
||||
$this->assertContains('Import successful: Imported campaigns are switched off by default.', $decoded);
|
||||
}
|
||||
|
||||
public function testImportCampaignAccessDenied(): void
|
||||
{
|
||||
$userWithoutPermission = $this->createUserWithPermission([
|
||||
'user-name' => 'no-import-user',
|
||||
'email' => 'no-import@mautic-test.com',
|
||||
'first-name' => 'NoImport',
|
||||
'last-name' => 'User',
|
||||
'role' => [
|
||||
'name' => 'no_import_role',
|
||||
'permissions' => [
|
||||
// Do not grant 'campaign:imports:create'
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginUser($userWithoutPermission);
|
||||
|
||||
// Attempt to import a campaign
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
// Assert that access is denied
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportCampaignNoFileUploaded(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Attempt to import with no files
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('No JSON content found and exactly one ZIP file must be uploaded.', $response->getContent());
|
||||
}
|
||||
|
||||
private function createTemporaryFile(string $extension): string
|
||||
{
|
||||
$filePath = tempnam(sys_get_temp_dir(), 'mautic_test_').'.'.$extension;
|
||||
file_put_contents($filePath, 'test content');
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
public function testImportCampaignInvalidFile(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary file
|
||||
$filePath = $this->createTemporaryFile('txt');
|
||||
|
||||
// Upload the invalid file
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($filePath, 'test.txt', null, null, true);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Unsupported file type. Only ZIP archives are supported.', $response->getContent());
|
||||
|
||||
// Clean up
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
public function testImportCampaignUnsupportedFileType(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary file with a non-ZIP extension
|
||||
$filePath = $this->createTemporaryFile('txt');
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($filePath, 'test.txt', null, null, true);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Unsupported file type. Only ZIP archives are supported.', $response->getContent());
|
||||
|
||||
// Clean up
|
||||
unlink($filePath);
|
||||
}
|
||||
|
||||
public function testImportCampaignMalformedJson(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
$this->loginUser($user);
|
||||
|
||||
// Create a temporary ZIP file with valid structure but malformed JSON
|
||||
$zipPath = tempnam(sys_get_temp_dir(), 'mautic_test_').'.zip';
|
||||
$zip = new \ZipArchive();
|
||||
if (true === $zip->open($zipPath, \ZipArchive::CREATE)) {
|
||||
// Add a valid JSON file with malformed content
|
||||
$zip->addFromString('campaign.json', '{invalid json content}');
|
||||
$zip->close();
|
||||
} else {
|
||||
$this->fail('Failed to create test ZIP file.');
|
||||
}
|
||||
|
||||
$file = new \Symfony\Component\HttpFoundation\File\UploadedFile($zipPath, 'test.zip', null, null, true);
|
||||
|
||||
try {
|
||||
$this->client->request(Request::METHOD_POST, '/api/campaigns/import', [], ['file' => $file]);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertEquals(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
|
||||
$this->assertStringContainsString('Invalid JSON', $response->getContent());
|
||||
} finally {
|
||||
// Clean up - check if file exists before trying to delete
|
||||
if (file_exists($zipPath)) {
|
||||
unlink($zipPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Lead as CampaignMember;
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ContactCampaignApiControllerFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
public function testContactCampaignApiEndpoints(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('campaign@tester.email');
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$campaignMemberRepository = $this->em->getRepository(CampaignMember::class);
|
||||
|
||||
// Add the contact to the campaign.
|
||||
$this->client->request(Request::METHOD_POST, "/api/campaigns/{$campaign->getId()}/contact/{$contact->getId()}/add");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
Assert::assertSame('{"success":1}', $clientResponse->getContent());
|
||||
|
||||
// Assert that the campaign member was really added.
|
||||
/** @var CampaignMember[] $campaignMembers */
|
||||
$campaignMembers = $campaignMemberRepository->findBy(['lead' => $contact->getId(), 'campaign' => $campaign->getId()]);
|
||||
Assert::assertCount(1, $campaignMembers);
|
||||
Assert::assertTrue($campaignMembers[0]->getManuallyAdded());
|
||||
Assert::assertFalse($campaignMembers[0]->getManuallyRemoved());
|
||||
|
||||
// Get the contact's campaigns.
|
||||
$this->client->request(Request::METHOD_GET, "/api/contacts/{$contact->getId()}/campaigns");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$body = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(1, $body['total'], $clientResponse->getContent());
|
||||
Assert::assertSame($campaign->getId(), $body['campaigns'][$campaign->getId()]['id'], $clientResponse->getContent());
|
||||
Assert::assertSame($campaign->getName(), $body['campaigns'][$campaign->getId()]['name'], $clientResponse->getContent());
|
||||
Assert::assertNotEmpty($body['campaigns'][$campaign->getId()]['dateAdded'], $clientResponse->getContent());
|
||||
Assert::assertFalse($body['campaigns'][$campaign->getId()]['manuallyRemoved'], $clientResponse->getContent());
|
||||
Assert::assertTrue($body['campaigns'][$campaign->getId()]['manuallyAdded'], $clientResponse->getContent());
|
||||
|
||||
// Get campaign contacts API endpoint.
|
||||
$this->client->request(Request::METHOD_GET, "/api/campaigns/{$campaign->getId()}/contacts");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$body = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(3, (int) $body['total']);
|
||||
Assert::assertSame($contact->getId(), (int) $body['contacts'][2]['lead_id']);
|
||||
|
||||
// Remove the contact from the campaign.
|
||||
$this->client->request(Request::METHOD_POST, "/api/campaigns/{$campaign->getId()}/contact/{$contact->getId()}/remove");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
Assert::assertSame('{"success":1}', $clientResponse->getContent());
|
||||
|
||||
// Assert that the campaign member was really removed.
|
||||
/** @var CampaignMember[] $campaignMembers */
|
||||
$campaignMembers = $campaignMemberRepository->findBy(['lead' => $contact->getId(), 'campaign' => $campaign->getId()]);
|
||||
Assert::assertCount(1, $campaignMembers);
|
||||
Assert::assertFalse($campaignMembers[0]->getManuallyAdded());
|
||||
Assert::assertTrue($campaignMembers[0]->getManuallyRemoved());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Command\SummarizeCommand;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Entity\LeadEventLog;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignControllerFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
private const CAMPAIGN_SUMMARY_PARAM = 'campaign_use_summary';
|
||||
|
||||
private const CAMPAIGN_RANGE_PARAM = 'campaign_by_range';
|
||||
|
||||
/**
|
||||
* @var CampaignModel
|
||||
*/
|
||||
private $campaignModel;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $campaignLeadsLabel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$functionForUseSummary = ['testCampaignContactCountThroughStatsWithSummary',
|
||||
'testCampaignContactCountOnCanvasWithSummaryWithoutRange', 'testCampaignContactCountOnCanvasWithSummaryAndRange',
|
||||
'testCampaignCountsBeforeSummarizeCommandWithSummaryWithoutRange', 'testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignCountsAfterSummarizeCommandWithSummaryWithoutRange', 'testCampaignCountsAfterSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignPendingCountsWithSummaryWithoutRange', 'testCampaignPendingCountsWithSummaryAndRange', 'testCampaignRemovedLeadCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange', ];
|
||||
$functionForUseRange = ['testCampaignContactCountOnCanvasWithoutSummaryWithRange', 'testCampaignContactCountOnCanvasWithSummaryAndRange',
|
||||
'testCampaignCountsBeforeSummarizeCommandWithoutSummaryWithRange', 'testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignCountsAfterSummarizeCommandWithoutSummaryWithRange', 'testCampaignCountsAfterSummarizeCommandWithSummaryAndRange',
|
||||
'testCampaignPendingCountsWithoutSummaryAndRange', 'testCampaignPendingCountsWithoutSummaryWithRange', 'testCampaignRemovedLeadCountsWithoutSummaryWithRange', 'testCampaignRemovedLeadCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange', 'testCampaignRemovedLeadAndPendingCountsWithoutSummaryWithRange', ];
|
||||
$this->configParams[self::CAMPAIGN_SUMMARY_PARAM] = in_array($this->name(), $functionForUseSummary);
|
||||
$this->configParams[self::CAMPAIGN_RANGE_PARAM] = in_array($this->name(), $functionForUseRange);
|
||||
parent::setUp();
|
||||
|
||||
$model = static::getContainer()->get(CampaignModel::class);
|
||||
|
||||
$this->campaignModel = $model;
|
||||
$this->campaignLeadsLabel = static::getContainer()->get('translator')->trans('mautic.campaign.campaign.leads');
|
||||
$this->configParams['delete_campaign_event_log_in_background'] = false;
|
||||
}
|
||||
|
||||
public function testCampaignContactCountThroughStatsWithSummary(): void
|
||||
{
|
||||
$this->campaignContactCountThroughStats();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountThroughStatsWithoutSummary(): void
|
||||
{
|
||||
$this->campaignContactCountThroughStats();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignContactCountOnCanvasWithSummaryAndRange(): void
|
||||
{
|
||||
$this->campaignContactCountOnCanvas();
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['0%', '0%'], ['0', '0'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsBeforeSummarizeCommandWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, false, ['0%', '0%'], ['0', '0'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignCountsAfterSummarizeCommandWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, false, true, ['100%', '100%'], ['2', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithoutSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithSummaryWithoutRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignPendingCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, false, true, ['100%', '100%'], ['3', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, true, true, ['100%', '100%'], ['3', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(false, true, true, ['100%', '100%'], ['3', '2'], ['0', '0']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadAndPendingCountsWithSummaryAndRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, true, true, ['100%', '100%'], ['4', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
public function testCampaignRemovedLeadAndPendingCountsWithoutSummaryWithRange(): void
|
||||
{
|
||||
$this->getCountAndDetails(true, true, true, ['100%', '100%'], ['4', '2'], ['0', '1']);
|
||||
}
|
||||
|
||||
private function getStatTotalContacts(int $campaignId): int
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
|
||||
$stats = $this->campaignModel->getCampaignMetricsLineChartData(
|
||||
null,
|
||||
new \DateTime($from),
|
||||
new \DateTime($to),
|
||||
null,
|
||||
['campaign_id' => $campaignId]
|
||||
);
|
||||
$datasets = $stats['datasets'] ?? [];
|
||||
|
||||
return $this->processTotalContactStats($datasets);
|
||||
}
|
||||
|
||||
private function getCanvasTotalContacts(int $campaignId): int
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$this->client->request('GET', sprintf('s/campaigns/graph/%d/%s/%s', $campaignId, $from, $to));
|
||||
$response = $this->client->getResponse();
|
||||
$body = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($body['newContent']);
|
||||
$canvasJson = trim($crawler->filter('canvas')->html());
|
||||
$canvasData = json_decode($canvasJson, true);
|
||||
$datasets = $canvasData['datasets'] ?? [];
|
||||
$this->client->restart();
|
||||
|
||||
return $this->processTotalContactStats($datasets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int|string>> $datasets
|
||||
*/
|
||||
private function processTotalContactStats(array $datasets): int
|
||||
{
|
||||
$totalContacts = 0;
|
||||
|
||||
foreach ($datasets as $dataset) {
|
||||
if ($dataset['label'] === $this->campaignLeadsLabel) {
|
||||
$data = $dataset['data'] ?? [];
|
||||
$totalContacts = array_sum($data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $totalContacts;
|
||||
}
|
||||
|
||||
private function getCrawlers(int $campaignId): Crawler
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$url = sprintf('s/campaigns/event/stats/%d/%s/%s', $campaignId, $from, $to);
|
||||
$this->client->request('GET', $url);
|
||||
$response = $this->client->getResponse();
|
||||
$body = json_decode($response->getContent(), true);
|
||||
$this->client->restart();
|
||||
|
||||
return new Crawler($body['actions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
private function getActionCounts(int $campaignId): array
|
||||
{
|
||||
$crawler = $this->getCrawlers($campaignId);
|
||||
$successPercent = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-success')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-success')->text()),
|
||||
];
|
||||
|
||||
$completed = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-warning')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-warning')->text()),
|
||||
];
|
||||
|
||||
$pending = [
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(1) .label-gray')->text()),
|
||||
trim($crawler->filter('.campaign-event-list li:nth-child(2) .label-gray')->text()),
|
||||
];
|
||||
|
||||
return [
|
||||
'successPercent' => $successPercent,
|
||||
'completed' => $completed,
|
||||
'pending' => $pending,
|
||||
];
|
||||
}
|
||||
|
||||
private function campaignContactCountThroughStats(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$campaignId = $campaign->getId();
|
||||
|
||||
$totalContacts = $this->getStatTotalContacts($campaignId);
|
||||
Assert::assertSame(2, $totalContacts);
|
||||
}
|
||||
|
||||
private function campaignContactCountOnCanvas(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$campaignId = $campaign->getId();
|
||||
$totalContacts = $this->getCanvasTotalContacts($campaignId);
|
||||
Assert::assertSame(2, $totalContacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $expectedSuccessPercent
|
||||
* @param array<int, string> $expectedCompleted
|
||||
* @param array<int, string> $expectedPending
|
||||
*/
|
||||
private function getCountAndDetails(bool $withPendingAction, bool $withActionOfRemovedLead, bool $runCommand, array $expectedSuccessPercent, array $expectedCompleted, array $expectedPending): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs($withPendingAction, $withActionOfRemovedLead);
|
||||
$campaignId = $campaign->getId();
|
||||
|
||||
if ($runCommand) {
|
||||
$this->testSymfonyCommand(
|
||||
SummarizeCommand::NAME,
|
||||
[
|
||||
'--env' => 'test',
|
||||
'--max-hours' => 768,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$actionCounts = $this->getActionCounts($campaignId);
|
||||
Assert::assertSame($expectedSuccessPercent, $actionCounts['successPercent']);
|
||||
Assert::assertSame($expectedCompleted, $actionCounts['completed']);
|
||||
Assert::assertSame($expectedPending, $actionCounts['pending']);
|
||||
}
|
||||
|
||||
public function testDeleteCampaign(): void
|
||||
{
|
||||
$lead = $this->createLead();
|
||||
$campaign = $this->createCampaign();
|
||||
$event = $this->createEvent('Event 1', $campaign);
|
||||
$this->createEventLog($lead, $event, $campaign);
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/s/campaigns/delete/'.$campaign->getId());
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$eventLogs = $this->em->getRepository(LeadEventLog::class)->findAll();
|
||||
Assert::assertCount(0, $eventLogs);
|
||||
}
|
||||
|
||||
private function createLead(): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('Test');
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(string $name, Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createEventLog(Lead $lead, Event $event, Campaign $campaign): LeadEventLog
|
||||
{
|
||||
$leadEventLog = new LeadEventLog();
|
||||
$leadEventLog->setLead($lead);
|
||||
$leadEventLog->setEvent($event);
|
||||
$leadEventLog->setCampaign($campaign);
|
||||
$this->em->persist($leadEventLog);
|
||||
$this->em->flush();
|
||||
|
||||
return $leadEventLog;
|
||||
}
|
||||
|
||||
public function testCampaignView(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/view/%d', $campaign->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
self::assertStringContainsString('Campaign ABC', $response->getContent());
|
||||
self::assertSame('', trim($crawler->filter('#decisions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#actions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#conditions-container')->text()));
|
||||
self::assertSame('', trim($crawler->filter('#campaign-graph-div')->text()));
|
||||
}
|
||||
|
||||
public function testCampaignViewEvents(): void
|
||||
{
|
||||
$from = date('Y-m-d', strtotime('-2 months'));
|
||||
$to = date('Y-m-d', strtotime('-1 month'));
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$this->client->request('GET', sprintf('s/campaigns/event/stats/%d/%s/%s', $campaign->getId(), $from, $to));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
$body = json_decode($response->getContent(), true);
|
||||
self::assertCount(2, $body);
|
||||
self::arrayHasKey('actions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* Index should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenNotFiltered(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns');
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenFiltering(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns?search=has%3Aresults&tmpl=list');
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get campaign's create page.
|
||||
*/
|
||||
public function testNewActionCampaign(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/campaigns/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cancelling new campaign does not give a 500 error.
|
||||
*
|
||||
* @see https://github.com/mautic/mautic/issues/11181
|
||||
*/
|
||||
public function testNewActionCampaignCancel(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/new/');
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->filter('form[name="campaign"]')->selectButton('campaign_buttons_cancel')->form();
|
||||
$this->client->submit($form);
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testCampaignWithProject(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test Campaign');
|
||||
$this->em->persist($campaign);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/edit/'.$campaign->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['campaign[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedCampaign = $this->em->find(Campaign::class, $campaign->getId());
|
||||
Assert::assertSame($project->getId(), $savedCampaign->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CampaignBundle\Controller\CampaignMapStatsController;
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CampaignBundle\Model\CampaignModel;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\CoreBundle\Helper\MapHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CampaignMapStatsControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private MockObject $campaignModelMock;
|
||||
|
||||
private CampaignMapStatsController $mapController;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->campaignModelMock = $this->createMock(CampaignModel::class);
|
||||
$this->mapController = new CampaignMapStatsController($this->campaignModelMock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, array<string, string>>>
|
||||
*/
|
||||
private function getStats(): array
|
||||
{
|
||||
return [
|
||||
'contacts' => [
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'contacts' => '4',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
'clicked_through_count' => [
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'clicked_through_count' => '4',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
'read_count' => [
|
||||
[
|
||||
'read_count' => '4',
|
||||
'country' => '',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Spain',
|
||||
],
|
||||
[
|
||||
'read_count' => '8',
|
||||
'country' => 'Finland',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testMapCountries(): void
|
||||
{
|
||||
$stats = $this->getStats();
|
||||
$reads = MapHelper::mapCountries($stats['read_count'], 'read_count');
|
||||
$clicks = MapHelper::mapCountries($stats['clicked_through_count'], 'clicked_through_count');
|
||||
|
||||
$this->assertSame([
|
||||
'data' => [
|
||||
'ES' => 8,
|
||||
'FI' => 8,
|
||||
],
|
||||
'total' => 20,
|
||||
'totalWithCountry' => 16,
|
||||
], $reads);
|
||||
|
||||
$this->assertSame([
|
||||
'data' => [
|
||||
'ES' => 4,
|
||||
'FI' => 4,
|
||||
],
|
||||
'total' => 12,
|
||||
'totalWithCountry' => 8,
|
||||
], $clicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testViewAction(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', "s/campaign-map-stats/{$campaign->getId()}/2023-07-20/2023-07-25");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$crawler = new Crawler($clientResponse->getContent(), $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertEmpty($crawler->filter('.map-options__title'));
|
||||
$this->assertCount(1, $crawler->filter('div.map-options'));
|
||||
$this->assertCount(1, $crawler->filter('div.vector-map'));
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testViewActionWithEmail(): void
|
||||
{
|
||||
$leadsPayload = [
|
||||
[
|
||||
'email' => 'test1@test.com',
|
||||
'country' => '',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'test2@test.com',
|
||||
'country' => '',
|
||||
'read' => true,
|
||||
'click' => false,
|
||||
],
|
||||
[
|
||||
'email' => 'example1@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => false,
|
||||
'click' => false,
|
||||
],
|
||||
[
|
||||
'email' => 'example2@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example3@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => true,
|
||||
],
|
||||
[
|
||||
'email' => 'example4@example.com',
|
||||
'country' => 'Spain',
|
||||
'read' => true,
|
||||
'click' => false,
|
||||
],
|
||||
];
|
||||
$campaign = $this->createCampaignWithEmail($leadsPayload);
|
||||
|
||||
$this->client->request('GET', "s/campaign-map-stats/{$campaign->getId()}/2023-07-20/2023-07-25");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$crawler = new Crawler($clientResponse->getContent(), $this->client->getInternalRequest()->getUri());
|
||||
|
||||
$this->assertEmpty($crawler->filter('.map-options__title'));
|
||||
$this->assertCount(1, $crawler->filter('div.map-options'));
|
||||
$this->assertCount(1, $crawler->filter('div.vector-map'));
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
$readOption = $crawler->filter('label.map-options__item')->filter('[data-stat-unit="Read"]');
|
||||
$this->assertCount(1, $readOption);
|
||||
$this->assertSame('Total: 5 (3 with country)', $readOption->attr('data-legend-text'));
|
||||
$this->assertSame('{"ES":3}', $readOption->attr('data-map-series'));
|
||||
|
||||
$clickOption = $crawler->filter('label.map-options__item')->filter('[data-stat-unit="Click"]');
|
||||
$this->assertCount(1, $clickOption);
|
||||
$this->assertSame('Total: 3 (2 with country)', $clickOption->attr('data-legend-text'));
|
||||
$this->assertSame('{"ES":2}', $clickOption->attr('data-map-series'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testGetMapOptionsEmailCampaign(): void
|
||||
{
|
||||
$campaign = $this->createCampaignWithEmail();
|
||||
|
||||
$result = $this->mapController->getMapOptions($campaign);
|
||||
$this->assertSame(CampaignMapStatsController::MAP_OPTIONS, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testGetMapOptionsNotEmailCampaign(): void
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign 1');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$result = $this->mapController->getMapOptions($campaign);
|
||||
$this->assertSame(['contacts' => CampaignMapStatsController::MAP_OPTIONS['contacts']], $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, bool|string>> $leadsPayload
|
||||
*
|
||||
* @throws ORMException
|
||||
* @throws OptimisticLockException
|
||||
*/
|
||||
private function createCampaignWithEmail(array $leadsPayload = []): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('Test campaign');
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// Create email
|
||||
$email = new Email();
|
||||
$email->setName('Test email');
|
||||
$this->em->persist($email);
|
||||
$this->em->flush();
|
||||
|
||||
// Create email events
|
||||
$event = new Event();
|
||||
$event->setName('Send email');
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setChannel('email');
|
||||
$event->setChannelId($email->getId());
|
||||
$event->setCampaign($campaign);
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
// Add events to campaign
|
||||
$campaign->addEvent(0, $event);
|
||||
|
||||
if (!empty($leadsPayload)) {
|
||||
$this->emulateEmailCampaignStat($event, $email, $leadsPayload);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, bool|string>> $leadsPayload
|
||||
*
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function emulateEmailCampaignStat(Event $event, Email $email, array $leadsPayload): void
|
||||
{
|
||||
foreach ($leadsPayload as $l) {
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($l['email']);
|
||||
$lead->setCountry($l['country']);
|
||||
$this->em->persist($lead);
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setEmailAddress('test-a@test.com');
|
||||
$stat->setLead($lead);
|
||||
$stat->setDateSent(new \DateTime('2023-07-22'));
|
||||
$stat->setEmail($email);
|
||||
$stat->setIsRead($l['read']);
|
||||
$stat->setSource('campaign.event');
|
||||
$stat->setSourceId($event->getId());
|
||||
$this->em->persist($stat);
|
||||
$this->em->flush();
|
||||
|
||||
if ($l['read'] && $l['click']) {
|
||||
$this->emulateClick($lead, $email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function emulateClick(Lead $lead, Email $email): void
|
||||
{
|
||||
$ipAddress = new IpAddress();
|
||||
$ipAddress->setIpAddress('127.0.0.1');
|
||||
$this->em->persist($ipAddress);
|
||||
$this->em->flush();
|
||||
|
||||
$redirect = new Redirect();
|
||||
$redirect->setRedirectId(uniqid());
|
||||
$redirect->setUrl('https://example.com');
|
||||
$redirect->setUniqueHits(1);
|
||||
$redirect->setHits(1);
|
||||
$this->em->persist($redirect);
|
||||
|
||||
$trackable = new Trackable();
|
||||
$trackable->setChannelId($email->getId());
|
||||
$trackable->setHits(1);
|
||||
$trackable->setChannel('email');
|
||||
$trackable->setUniqueHits(1);
|
||||
$trackable->setRedirect($redirect);
|
||||
$this->em->persist($trackable);
|
||||
|
||||
$pageHit = new Hit();
|
||||
$pageHit->setRedirect($redirect);
|
||||
$pageHit->setIpAddress($ipAddress);
|
||||
$pageHit->setEmail($email);
|
||||
$pageHit->setLead($lead);
|
||||
$pageHit->setDateHit(new \DateTime('2023-07-22'));
|
||||
$pageHit->setCode(200);
|
||||
$pageHit->setUrl($redirect->getUrl());
|
||||
$pageHit->setTrackingId($redirect->getRedirectId());
|
||||
$pageHit->setSource('email');
|
||||
$pageHit->setSourceId($email->getId());
|
||||
$this->em->persist($pageHit);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\Functional\Fixtures\FixtureHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\EmailBundle\Tests\Functional\Fixtures\EmailFixturesHelper;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class CampaignMetricsControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private FixtureHelper $campaignFixturesHelper;
|
||||
private EmailFixturesHelper $emailFixturesHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->campaignFixturesHelper = new FixtureHelper($this->em);
|
||||
$this->emailFixturesHelper = new EmailFixturesHelper($this->em);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function setupEmailCampaignTestData(): array
|
||||
{
|
||||
$contacts = [
|
||||
$this->campaignFixturesHelper->createContact('john@example.com'),
|
||||
$this->campaignFixturesHelper->createContact('paul@example.com'),
|
||||
];
|
||||
|
||||
$email = $this->emailFixturesHelper->createEmail('Test Email');
|
||||
$this->em->flush();
|
||||
|
||||
$campaign = $this->campaignFixturesHelper->createCampaignWithEmailSent($email->getId());
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contacts[0], $campaign);
|
||||
$this->campaignFixturesHelper->addContactToCampaign($contacts[1], $campaign);
|
||||
$eventId = $campaign->getEmailSendEvents()->first()->getId();
|
||||
|
||||
$emailStats = [
|
||||
$this->emailFixturesHelper->emulateEmailSend($contacts[0], $email, '2024-12-10 12:00:00', 'campaign.event', $eventId),
|
||||
$this->emailFixturesHelper->emulateEmailSend($contacts[1], $email, '2024-12-10 12:00:00', 'campaign.event', $eventId),
|
||||
];
|
||||
|
||||
$this->emailFixturesHelper->emulateEmailRead($emailStats[0], $email, '2024-12-10 12:09:00');
|
||||
$this->emailFixturesHelper->emulateEmailRead($emailStats[1], $email, '2024-12-11 21:35:00');
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->persist($email);
|
||||
|
||||
$emailLinks = [
|
||||
$this->emailFixturesHelper->createEmailLink('https://example.com/1', $email->getId()),
|
||||
$this->emailFixturesHelper->createEmailLink('https://example.com/2', $email->getId()),
|
||||
];
|
||||
$this->em->flush();
|
||||
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[0], $contacts[0], '2024-12-10 12:10:00', 3);
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[1], $contacts[0], '2024-12-10 13:20:00');
|
||||
$this->emailFixturesHelper->emulateLinkClick($email, $emailLinks[1], $contacts[1], '2024-12-11 21:37:00');
|
||||
$this->em->flush();
|
||||
|
||||
return ['campaign' => $campaign, 'email' => $email];
|
||||
}
|
||||
|
||||
public function testEmailWeekdaysAction(): void
|
||||
{
|
||||
$testData = $this->setupEmailCampaignTestData();
|
||||
$campaign = $testData['campaign'];
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/campaign/metrics/email-weekdays/{$campaign->getId()}/2024-12-01/2024-12-12");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$crawler = new Crawler($content);
|
||||
$daysJson = $crawler->filter('canvas')->text(null, false);
|
||||
$daysData = json_decode(html_entity_decode($daysJson), true);
|
||||
$daysDatasets = $daysData['datasets'];
|
||||
Assert::assertIsArray($daysDatasets);
|
||||
Assert::assertCount(3, $daysDatasets); // Assuming there are 3 datasets: Email sent, Email read, Email clicked
|
||||
|
||||
$expectedDaysLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
$expectedDaysData = [
|
||||
['label' => 'Email sent', 'data' => [0, 2, 0, 0, 0, 0, 0]],
|
||||
['label' => 'Email read', 'data' => [0, 1, 1, 0, 0, 0, 0]],
|
||||
['label' => 'Email clicked', 'data' => [0, 4, 1, 0, 0, 0, 0]],
|
||||
];
|
||||
Assert::assertEquals($expectedDaysLabels, $daysData['labels']);
|
||||
foreach ($daysDatasets as $index => $dataset) {
|
||||
Assert::assertEquals($expectedDaysData[$index]['label'], $dataset['label']);
|
||||
Assert::assertEquals($expectedDaysData[$index]['data'], $dataset['data']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testEmailHoursAction(): void
|
||||
{
|
||||
$testData = $this->setupEmailCampaignTestData();
|
||||
$campaign = $testData['campaign'];
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/campaign/metrics/email-hours/{$campaign->getId()}/2024-12-01/2024-12-12");
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$content = $this->client->getResponse()->getContent();
|
||||
$crawler = new Crawler($content);
|
||||
$hourJson = $crawler->filter('canvas')->text(null, false);
|
||||
$hoursData = json_decode(html_entity_decode($hourJson), true);
|
||||
|
||||
$hoursDatasets = $hoursData['datasets'];
|
||||
Assert::assertIsArray($hoursDatasets);
|
||||
Assert::assertCount(3, $hoursDatasets); // Assuming there are 3 datasets: Email sent, Email read, Email clicked
|
||||
|
||||
// Get the time format from CoreParametersHelper
|
||||
$coreParametersHelper = self::getContainer()->get('mautic.helper.core_parameters');
|
||||
$timeFormat = $coreParametersHelper->get('date_format_timeonly');
|
||||
|
||||
// Generate expected hour labels based on the actual time format
|
||||
$expectedHoursLabels = [];
|
||||
for ($hour = 0; $hour < 24; ++$hour) {
|
||||
$startTime = (new \DateTime())->setTime($hour, 0);
|
||||
$endTime = (new \DateTime())->setTime(($hour + 1) % 24, 0);
|
||||
$expectedHoursLabels[] = $startTime->format($timeFormat).' - '.$endTime->format($timeFormat);
|
||||
}
|
||||
|
||||
Assert::assertEquals($expectedHoursLabels, $hoursData['labels']);
|
||||
|
||||
$expectedHoursData = [
|
||||
['label' => 'Email sent', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],
|
||||
['label' => 'Email read', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
|
||||
['label' => 'Email clicked', 'data' => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]],
|
||||
];
|
||||
foreach ($hoursDatasets as $index => $dataset) {
|
||||
Assert::assertEquals($expectedHoursData[$index]['label'], $dataset['label']);
|
||||
Assert::assertEquals($expectedHoursData[$index]['data'], $dataset['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class CampaignProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$campaignAlpha = $this->createCampaign('Campaign Alpha');
|
||||
$campaignBeta = $this->createCampaign('Campaign Beta');
|
||||
$this->createCampaign('Campaign Gamma');
|
||||
$this->createCampaign('Campaign Delta');
|
||||
|
||||
$campaignAlpha->addProject($projectOne);
|
||||
$campaignAlpha->addProject($projectTwo);
|
||||
$campaignBeta->addProject($projectTwo);
|
||||
$campaignBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/campaigns', '/s/campaigns']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND campaign name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR campaign name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Beta', 'Campaign Gamma'],
|
||||
'unexpectedEntities' => ['Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Beta'],
|
||||
'unexpectedEntities' => ['Campaign Gamma', 'Campaign Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Campaign Alpha', 'Campaign Gamma', 'Campaign Delta'],
|
||||
'unexpectedEntities' => ['Campaign Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createCampaign(string $name): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName($name);
|
||||
$this->em->persist($campaign);
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Tests\Campaign\AbstractCampaignTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class CampaignUnpublishedWorkflowFunctionalTest extends AbstractCampaignTestCase
|
||||
{
|
||||
public function testCreateCampaignPageShouldNotContainConformation(): void
|
||||
{
|
||||
// Check the message in the Campaign edit page
|
||||
$crawler = $this->client->request('GET', '/s/campaigns/new');
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'data-toggle',
|
||||
'data-message',
|
||||
'data-confirm-text',
|
||||
'data-confirm-callback',
|
||||
'data-cancel-text',
|
||||
'data-cancel-callback',
|
||||
];
|
||||
|
||||
$elements = $crawler->filter('form input[name*="campaign[isPublished]"]')->getIterator();
|
||||
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
foreach ($attributes as $attribute) {
|
||||
$this->assertFalse($element->hasAttribute($attribute), sprintf('The "%s" attribute is present.', $attribute));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignEditPageCheckUnpublishWorkflowAttributesPresent(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Check the message in the Campaign edit page
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/edit/%d', $campaign->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onchange' => 'Mautic.showCampaignConfirmation(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-confirm-callback' => 'dismissConfirmation',
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
'data-cancel-callback' => 'setPublishedButtonToYes',
|
||||
];
|
||||
|
||||
$elements = $crawler->filter('form input[name*="campaign[isPublished]"]')->getIterator();
|
||||
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($val, $element->getAttribute($key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignListPageCheckUnpublishWorkflowAttributesPresent(): void
|
||||
{
|
||||
$this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
// Check the message in the Campaign listing page
|
||||
$crawler = $this->client->request('GET', sprintf('/s/campaigns'));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onclick' => 'Mautic.confirmationCampaignPublishStatus(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
|
||||
'data-cancel-callback' => 'dismissConfirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
];
|
||||
|
||||
$toggleElement = $crawler->filter('.toggle-publish-status');
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($val, $toggleElement->attr($key));
|
||||
}
|
||||
}
|
||||
|
||||
public function testCampaignUnpublishToggle(): void
|
||||
{
|
||||
$campaign = $this->saveSomeCampaignLeadEventLogs();
|
||||
$translator = static::getContainer()->get('translator');
|
||||
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'campaign', 'id' => $campaign->getId()]);
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
$this->assertTrue($response->isOk());
|
||||
|
||||
$attributes = [
|
||||
'onclick' => 'Mautic.confirmationCampaignPublishStatus(mQuery(this));',
|
||||
'data-toggle' => 'confirmation',
|
||||
'data-confirm-callback' => 'confirmCallbackCampaignPublishStatus',
|
||||
'data-cancel-callback' => 'dismissConfirmation',
|
||||
'data-message' => $translator->trans('mautic.campaign.form.confirmation.message'),
|
||||
'data-confirm-text' => $translator->trans('mautic.campaign.form.confirmation.confirm_text'),
|
||||
'data-cancel-text' => $translator->trans('mautic.campaign.form.confirmation.cancel_text'),
|
||||
];
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
foreach ($attributes as $key => $val) {
|
||||
$this->assertStringContainsString($key, $content);
|
||||
$this->assertStringContainsString($val, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CampaignBundle\Entity\Event;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class EventControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('fieldAndValueProvider')]
|
||||
public function testCreateContactConditionOnStateField(string $field, string $value): void
|
||||
{
|
||||
// Fetch the campaign condition form.
|
||||
$uri = '/s/campaigns/events/new?type=lead.field_value&eventType=condition&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=leadsource&anchorEventType=source';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[anchor]' => 'leadsource',
|
||||
'campaignevent[properties][field]' => $field,
|
||||
'campaignevent[properties][operator]' => '=',
|
||||
'campaignevent[properties][value]' => $value,
|
||||
'campaignevent[type]' => 'lead.field_value',
|
||||
'campaignevent[eventType]' => 'condition',
|
||||
'campaignevent[anchorEventType]' => 'source',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$actualEventData = array_filter($responseData['event'], fn ($value) => in_array($value, [
|
||||
'name',
|
||||
'type',
|
||||
'eventType',
|
||||
'anchor',
|
||||
'anchorEventType',
|
||||
]), ARRAY_FILTER_USE_KEY);
|
||||
$expectedEventData = [
|
||||
'name' => 'Contact field value',
|
||||
'type' => 'lead.field_value',
|
||||
'eventType' => 'condition',
|
||||
'anchor' => 'leadsource',
|
||||
'anchorEventType' => 'source',
|
||||
];
|
||||
|
||||
$this->assertSame($expectedEventData, $actualEventData);
|
||||
$this->assertSame('condition', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
Assert::assertTrue($responseData['formSubmitted'], $response->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function fieldAndValueProvider(): array
|
||||
{
|
||||
return [
|
||||
'country' => ['country', 'India'],
|
||||
'region' => ['state', 'Arizona'],
|
||||
'timezone' => ['timezone', 'Marigot'],
|
||||
'locale' => ['preferred_locale', 'af'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testActionAtSpecificTimeWorkflow(): void
|
||||
{
|
||||
$uri = '/s/campaigns/events/new?type=lead.changepoints&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '',
|
||||
'campaignevent[triggerMode]' => 'date',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '21',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$this->assertNotEmpty($responseData['eventId']);
|
||||
$this->assertNotEmpty($responseData['event']['id']);
|
||||
$this->assertEquals($responseData['eventId'], $responseData['event']['id']);
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame('by September 27, 2023 9:37 pm UTC', $responseData['label']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
$this->assertArrayHasKey('eventHtml', $responseData);
|
||||
$this->assertArrayNotHasKey('updateHtml', $responseData);
|
||||
$eventId = $responseData['event']['id'];
|
||||
$modifiedEvents = $responseData['modifiedEvents'] ?? [];
|
||||
|
||||
// GET EDIT FORM
|
||||
$uri = "/s/campaigns/events/edit/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition";
|
||||
$this->client->xmlHttpRequest('GET', $uri, ['modifiedEvents' => json_encode($modifiedEvents)]);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// FILL EDIT FORM
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '2 contact points after 1 day',
|
||||
'campaignevent[triggerMode]' => 'interval',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '2',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$formData = $form->getPhpValues();
|
||||
$formData['modifiedEvents'] = json_encode($modifiedEvents);
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertTrue($responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
|
||||
$this->assertEquals($eventId, $responseData['eventId']);
|
||||
$this->assertEquals($eventId, $responseData['event']['id']);
|
||||
$this->assertSame('2 contact points after 1 day', $responseData['event']['name']);
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertSame('within 1 day', $responseData['label']);
|
||||
$this->assertSame(1, $responseData['closeModal']);
|
||||
$this->assertArrayHasKey('updateHtml', $responseData);
|
||||
$this->assertArrayNotHasKey('eventHtml', $responseData);
|
||||
}
|
||||
|
||||
public function testCloneWorkflow(): void
|
||||
{
|
||||
$uri = '/s/campaigns/events/new?type=lead.changepoints&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=no&anchorEventType=condition';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[canvasSettings][droppedX]' => '863',
|
||||
'campaignevent[canvasSettings][droppedY]' => '363',
|
||||
'campaignevent[name]' => '',
|
||||
'campaignevent[triggerMode]' => 'date',
|
||||
'campaignevent[triggerDate]' => '2023-09-27 21:37',
|
||||
'campaignevent[triggerInterval]' => '1',
|
||||
'campaignevent[triggerIntervalUnit]' => 'd',
|
||||
'campaignevent[triggerHour]' => '',
|
||||
'campaignevent[triggerRestrictedStartHour]' => '',
|
||||
'campaignevent[triggerRestrictedStopHour]' => '',
|
||||
'campaignevent[anchor]' => 'no',
|
||||
'campaignevent[properties][points]' => '21',
|
||||
'campaignevent[properties][group]' => '',
|
||||
'campaignevent[type]' => 'lead.changepoints',
|
||||
'campaignevent[eventType]' => 'action',
|
||||
'campaignevent[anchorEventType]' => 'condition',
|
||||
'campaignevent[campaignId]' => 'mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$eventId = $responseData['event']['id'];
|
||||
|
||||
// CLONE EVENT
|
||||
$uri = "/s/campaigns/events/clone/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775";
|
||||
$this->client->xmlHttpRequest('POST', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$this->assertSame('campaignEventClone', $responseData['mauticContent']);
|
||||
$this->assertSame('Adjust contact points', $responseData['eventName']);
|
||||
$this->assertSame('New campaign', $responseData['campaignName']);
|
||||
|
||||
// INSERT EVENT
|
||||
$uri = "/s/campaigns/events/insert/{$eventId}?campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775";
|
||||
$this->client->xmlHttpRequest('POST', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$this->assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
$this->assertSame('action', $responseData['eventType']);
|
||||
$this->assertSame('campaignEvent', $responseData['mauticContent']);
|
||||
$this->assertTrue($responseData['clearCloneStorage']);
|
||||
$this->assertNotEquals($eventId, $responseData['eventId']);
|
||||
$this->assertNotEmpty($responseData['eventHtml']);
|
||||
}
|
||||
|
||||
public function testEmailSendTypeDefaultSetting(): void
|
||||
{
|
||||
// Fetch the campaign action form.
|
||||
$uri = '/s/campaigns/events/new?type=email.send&eventType=action&campaignId=mautic_89f7f52426c1dff3daa3beaea708a6b39fe7a775&anchor=leadsource&anchorEventType=source';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
|
||||
// Assert the field email_type === "marketing"
|
||||
Assert::assertEquals('marketing', $form['campaignevent[properties][email_type]']->getValue(), 'The default email type should be "marketing"');
|
||||
}
|
||||
|
||||
public function testEventsAreNotAccessibleWithXhr(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/edit/'.$event1->getId().'?campaignId='.$campaign->getId(),
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
'You do not have access to the requested area/action.',
|
||||
$response['error']
|
||||
);
|
||||
}
|
||||
|
||||
public function testEventsAreAccessible(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/edit/'.$event1->getId().'?campaignId='.$campaign->getId(),
|
||||
[],
|
||||
[],
|
||||
$this->createAjaxHeaders(),
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
$event1->getId(),
|
||||
$response['eventId']
|
||||
);
|
||||
Assert::assertSame(
|
||||
$event1->getName(),
|
||||
$response['event']['name']
|
||||
);
|
||||
Assert::assertFalse($response['formSubmitted'], $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testEventsAreDeleted(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$event1 = $this->createEvent('Event1', $campaign);
|
||||
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/campaigns/events/delete/'.$event1->getId(),
|
||||
[
|
||||
'modifiedEvents' => json_encode([
|
||||
$event1->getId() => [
|
||||
'id' => $event1->getId(),
|
||||
'eventType' => $event1->getEventType(),
|
||||
'type' => $event1->getType(),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[],
|
||||
$this->createAjaxHeaders(),
|
||||
'{}'
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$response = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(
|
||||
1,
|
||||
$response['success']
|
||||
);
|
||||
Assert::assertContains(
|
||||
(string) $event1->getId(),
|
||||
$response['deletedEvents']
|
||||
);
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('My campaign');
|
||||
$campaign->setIsPublished(true);
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createEvent(string $name, Campaign $campaign): Event
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setName($name);
|
||||
$event->setCampaign($campaign);
|
||||
$event->setType('email.send');
|
||||
$event->setEventType('action');
|
||||
$event->setTriggerInterval(1);
|
||||
$event->setTriggerMode('immediate');
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\FormBundle\Entity\Form;
|
||||
|
||||
class SourceControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const ACCESS_DENIED = 'You do not have access to the requested area\/action';
|
||||
private const NEW_FORMS_URL = '/s/campaigns/sources/new/1?sourceType=forms';
|
||||
private const DELETE_FORMS_URL = '/s/campaigns/sources/delete/1?sourceType=forms';
|
||||
|
||||
public function testNewActionWithInvalidSourceType(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest('GET', '/s/campaigns/sources/new/1?sourceType=invalid');
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString(self::ACCESS_DENIED, $response->getContent());
|
||||
}
|
||||
|
||||
public function testNewActionWithNonAjaxRequest(): void
|
||||
{
|
||||
$this->client->request('GET', self::NEW_FORMS_URL);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertStringContainsString(self::ACCESS_DENIED, $response->getContent());
|
||||
}
|
||||
|
||||
public function testNewActionFormCancelled(): void
|
||||
{
|
||||
$formData = [
|
||||
'campaign_leadsource' => [
|
||||
'sourceType' => 'forms',
|
||||
],
|
||||
'submit' => '1',
|
||||
'cancel' => '1',
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', self::NEW_FORMS_URL, $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertArrayHasKey('mauticContent', $json, 'Response should contain mauticContent key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
$this->assertJsonResponseEquals('mauticContent', 'campaignSource', $json);
|
||||
// When cancelled, we expect the form to be returned with error state
|
||||
$this->assertArrayHasKey('newContent', $json, 'Response should contain form HTML when validation fails');
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testNewActionFormInvalid(): void
|
||||
{
|
||||
$formData = [
|
||||
'campaign_leadsource' => [
|
||||
'sourceType' => 'forms',
|
||||
],
|
||||
'submit' => '1',
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', self::NEW_FORMS_URL, $formData);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
$this->assertArrayHasKey('mauticContent', $json, 'Response should contain mauticContent key');
|
||||
$this->assertArrayHasKey('newContent', $json, 'Response should contain form HTML when validation fails');
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testDeleteActionWithGetRequest(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest('GET', self::DELETE_FORMS_URL);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
if (is_array($json)) {
|
||||
$this->assertArrayHasKey('success', $json, 'Response should contain success key');
|
||||
$this->assertJsonResponseEquals('success', 0, $json);
|
||||
} else {
|
||||
$this->fail('Response is not valid JSON: '.$response->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
public function testTwoSourcesWithSameName(): void
|
||||
{
|
||||
$form1 = new Form();
|
||||
$form1->setName('test');
|
||||
$form1->setAlias('test');
|
||||
$form1->setFormType('campaign');
|
||||
|
||||
$form2 = new Form();
|
||||
$form2->setName('test');
|
||||
$form2->setAlias('test');
|
||||
$form2->setFormType('campaign');
|
||||
|
||||
$this->em->persist($form1);
|
||||
$this->em->persist($form2);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->detach($form1);
|
||||
$this->em->detach($form2);
|
||||
|
||||
$this->client->xmlHttpRequest('GET', '/s/campaigns/sources/new/random_object_id?sourceType=forms');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseContent = $clientResponse->getContent();
|
||||
$this->assertResponseIsSuccessful($responseContent);
|
||||
|
||||
$html = json_decode($responseContent, true)['newContent'];
|
||||
$this->assertStringContainsString("<option value=\"{$form1->getId()}\">test ({$form1->getId()})</option>", $html);
|
||||
$this->assertStringContainsString("<option value=\"{$form2->getId()}\">test ({$form2->getId()})</option>", $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
private function assertJsonResponseHasKey(string $key, array $json, string $message = ''): void
|
||||
{
|
||||
$this->assertIsArray($json, 'Response is not a valid JSON array');
|
||||
$this->assertArrayHasKey($key, $json, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
private function assertJsonResponseEquals(string $key, mixed $expected, array $json, string $message = ''): void
|
||||
{
|
||||
$this->assertJsonResponseHasKey($key, $json, $message);
|
||||
$this->assertEquals($expected, $json[$key], $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\CampaignBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class VisitedPageConditionControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @param array<mixed,mixed> $pageUrl
|
||||
* @param array<mixed,mixed> $startDate
|
||||
* @param array<mixed,mixed> $endDate
|
||||
* @param array<mixed,mixed> $accumulativeTime
|
||||
* @param array<mixed,mixed> $page
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('fieldAndValueProvider')]
|
||||
public function testCreatePageHitConditionForm(
|
||||
array $pageUrl,
|
||||
array $startDate,
|
||||
array $endDate,
|
||||
array $accumulativeTime,
|
||||
array $page,
|
||||
): void {
|
||||
// Fetch the campaign condition form.
|
||||
$uri = 's/campaigns/events/new?type=lead.pageHit&eventType=condition&campaignId=3&anchor=leadsource&anchorEventType=source&_=1682493324393&mauticUserLastActive=897&mauticLastNotificationId=';
|
||||
$this->client->xmlHttpRequest('GET', $uri);
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Get the form HTML element out of the response, fill it in and submit.
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
$crawler = new Crawler($responseData['newContent'], $this->client->getInternalRequest()->getUri());
|
||||
$form = $crawler->filterXPath('//form[@name="campaignevent"]')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'campaignevent[anchor]' => 'leadsource',
|
||||
'campaignevent[properties]['.$pageUrl[0].']' => $pageUrl[1],
|
||||
'campaignevent[properties]['.$startDate[0].']' => $startDate[1],
|
||||
'campaignevent[properties]['.$endDate[0].']' => $endDate[1],
|
||||
'campaignevent[properties]['.$accumulativeTime[0].']' => $accumulativeTime[1],
|
||||
'campaignevent[properties]['.$page[0].']' => $page[1] ?? '',
|
||||
'campaignevent[type]' => 'lead.pageHit',
|
||||
'campaignevent[eventType]' => 'condition',
|
||||
'campaignevent[anchorEventType]' => 'source',
|
||||
'campaignevent[campaignId]' => '3',
|
||||
]
|
||||
);
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues());
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
Assert::assertSame(1, $responseData['success'], print_r(json_decode($response->getContent(), true), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed,mixed>
|
||||
*/
|
||||
public static function fieldAndValueProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'pageUrl' => ['page_url', 'https://example.com'],
|
||||
'startDate' => ['startDate', (new \DateTime())->format('Y-m-d H:i:s')],
|
||||
'endDate' => ['endDate', (new \DateTime())->modify('+ 5 days')->format('Y-m-d H:i:s')],
|
||||
'accumulativeTime' => ['accumulative_time', 5],
|
||||
'page' => ['page', null],
|
||||
],
|
||||
[
|
||||
'pageUrl' => ['page_url', 'https://example.com'],
|
||||
'startDate' => ['startDate', (new \DateTime())->format('Y-m-d H:i:s')],
|
||||
'endDate' => ['endDate', (new \DateTime())->modify('+ 10 days')->format('Y-m-d H:i:s')],
|
||||
'accumulativeTime' => ['accumulative_time', null],
|
||||
'page' => ['page', ''],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user