Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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