Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\RoleRepository;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use MauticPlugin\MauticTagManagerBundle\Entity\Tag;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement([
|
||||
'leads',
|
||||
'campaigns',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testToggleLeadCampaignAction(): void
|
||||
{
|
||||
$campaign = $this->createCampaign();
|
||||
$contact = $this->createContact('blabla@contact.email');
|
||||
|
||||
// Ensure there is no member for campaign 1 yet.
|
||||
$this->assertSame([], $this->getMembersForCampaign($campaign->getId()));
|
||||
|
||||
// Create the member now.
|
||||
$payload = [
|
||||
'action' => 'lead:toggleLeadCampaign',
|
||||
'leadId' => $contact->getId(),
|
||||
'campaignId' => $campaign->getId(),
|
||||
'campaignAction' => 'add',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
// Ensure the contact 1 is a campaign 1 member now.
|
||||
$this->assertSame(
|
||||
[['lead_id' => (string) $contact->getId(), 'manually_added' => '1', 'manually_removed' => '0']],
|
||||
$this->getMembersForCampaign($campaign->getId()),
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
|
||||
// Let's remove the member now.
|
||||
$payload = [
|
||||
'action' => 'lead:toggleLeadCampaign',
|
||||
'leadId' => $contact->getId(),
|
||||
'campaignId' => $campaign->getId(),
|
||||
'campaignAction' => 'remove',
|
||||
];
|
||||
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Ensure the contact 1 was removed as a member of campaign 1 member now.
|
||||
$this->assertSame([['lead_id' => (string) $contact->getId(), 'manually_added' => '0', 'manually_removed' => '1']], $this->getMembersForCampaign($campaign->getId()));
|
||||
|
||||
$this->assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTreeWithNotExistingSegment(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getSegmentDependencyTree&id=9999');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(404, $response->getStatusCode());
|
||||
Assert::assertSame('{"message":"Segment 9999 could not be found."}', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithNoCompanySelected(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=unicorn');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertSame('[]', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithCompanySelected(): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName('SaaS Company');
|
||||
$this->em->persist($company);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=sa');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertSame('[{"text":"SaaS Company","value":"'.$company->getId().'"}]', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithNoModelSet(): void
|
||||
{
|
||||
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&lead.company=unicorn');
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(400, $response->getStatusCode());
|
||||
Assert::assertStringContainsString('Bad Request - The searchKey parameter is required', $response->getContent());
|
||||
}
|
||||
|
||||
public function testCompanyLookupWithLimit(): void
|
||||
{
|
||||
$company1 = new Company();
|
||||
$company1->setName('Company 1');
|
||||
$this->em->persist($company1);
|
||||
|
||||
$company2 = new Company();
|
||||
$company2->setName('Company 2');
|
||||
$this->em->persist($company2);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=Company&limit=1');
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$content = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertIsArray($content);
|
||||
Assert::assertCount(1, $content, 'The result should contain only one element');
|
||||
Assert::assertSame('Company 1', $content[0]['text']);
|
||||
Assert::assertSame($company1->getId(), (int) $content[0]['value']);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:getLookupChoiceList&searchKey=lead.company&lead.company=Company&limit=1&start=1');
|
||||
$response = $this->client->getResponse();
|
||||
$content = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(200, $response->getStatusCode());
|
||||
Assert::assertIsArray($content);
|
||||
Assert::assertCount(1, $content, 'The result should contain only one element');
|
||||
Assert::assertSame('Company 2', $content[0]['text']);
|
||||
Assert::assertSame($company2->getId(), (int) $content[0]['value']);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTree(): void
|
||||
{
|
||||
$segmentA = new LeadList();
|
||||
$segmentA->setName('Segment A');
|
||||
$segmentA->setPublicName('Segment A');
|
||||
$segmentA->setAlias('segment-a');
|
||||
|
||||
$segmentB = new LeadList();
|
||||
$segmentB->setName('Segment B');
|
||||
$segmentB->setPublicName('Segment B');
|
||||
$segmentB->setAlias('segment-b');
|
||||
|
||||
$segmentC = new LeadList();
|
||||
$segmentC->setName('Segment C');
|
||||
$segmentC->setPublicName('Segment C');
|
||||
$segmentC->setAlias('segment-c');
|
||||
|
||||
$segmentD = new LeadList();
|
||||
$segmentD->setName('Segment D');
|
||||
$segmentD->setPublicName('Segment D');
|
||||
$segmentD->setAlias('segment-d');
|
||||
|
||||
$segmentE = new LeadList();
|
||||
$segmentE->setName('Segment E');
|
||||
$segmentE->setPublicName('Segment E');
|
||||
$segmentE->setAlias('segment-e');
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentB);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->persist($segmentD);
|
||||
$this->em->persist($segmentE);
|
||||
$this->em->flush();
|
||||
|
||||
$segmentA->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentB->getId()]],
|
||||
], [
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => '!in',
|
||||
'properties' => ['filter' => [$segmentC->getId(), $segmentD->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentC->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentE->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/ajax?action=lead:getSegmentDependencyTree&id={$segmentA->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk(), $response->getContent());
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'levels' => [
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "0-{$segmentA->getId()}", 'name' => $segmentA->getName(), 'link' => "/s/segments/view/{$segmentA->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentA->getId()}-{$segmentB->getId()}", 'name' => $segmentB->getName(), 'link' => "/s/segments/view/{$segmentB->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentC->getId()}", 'name' => $segmentC->getName(), 'link' => "/s/segments/view/{$segmentC->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentD->getId()}", 'name' => $segmentD->getName(), 'link' => "/s/segments/view/{$segmentD->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentC->getId()}-{$segmentE->getId()}", 'name' => $segmentE->getName(), 'link' => "/s/segments/view/{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
],
|
||||
'edges' => [
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentB->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentC->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentD->getId()}"],
|
||||
['source' => "{$segmentA->getId()}-{$segmentC->getId()}", 'target' => "{$segmentC->getId()}-{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
json_decode($response->getContent(), true)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSegmentDependencyTreeWithLoop(): void
|
||||
{
|
||||
$segmentA = new LeadList();
|
||||
$segmentA->setName('Segment A');
|
||||
$segmentA->setPublicName('Segment A');
|
||||
$segmentA->setAlias('segment-a');
|
||||
|
||||
$segmentB = new LeadList();
|
||||
$segmentB->setName('Segment B');
|
||||
$segmentB->setPublicName('Segment B');
|
||||
$segmentB->setAlias('segment-b');
|
||||
|
||||
$segmentC = new LeadList();
|
||||
$segmentC->setName('Segment C');
|
||||
$segmentC->setPublicName('Segment C');
|
||||
$segmentC->setAlias('segment-c');
|
||||
|
||||
$segmentD = new LeadList();
|
||||
$segmentD->setName('Segment D');
|
||||
$segmentD->setPublicName('Segment D');
|
||||
$segmentD->setAlias('segment-d');
|
||||
|
||||
$segmentE = new LeadList();
|
||||
$segmentE->setName('Segment E');
|
||||
$segmentE->setPublicName('Segment E');
|
||||
$segmentE->setAlias('segment-e');
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentB);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->persist($segmentD);
|
||||
$this->em->persist($segmentE);
|
||||
$this->em->flush();
|
||||
|
||||
$segmentA->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentB->getId()]],
|
||||
], [
|
||||
'object' => 'lead',
|
||||
'glue' => 'or',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => '!in',
|
||||
'properties' => ['filter' => [$segmentC->getId(), $segmentD->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentC->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentE->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentE->setFilters(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => ['filter' => [$segmentA->getId()]],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, "/s/ajax?action=lead:getSegmentDependencyTree&id={$segmentA->getId()}");
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk(), $response->getContent());
|
||||
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
Assert::assertSame(
|
||||
[
|
||||
'levels' => [
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "0-{$segmentA->getId()}", 'name' => $segmentA->getName(), 'link' => "/s/segments/view/{$segmentA->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentA->getId()}-{$segmentB->getId()}", 'name' => $segmentB->getName(), 'link' => "/s/segments/view/{$segmentB->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentC->getId()}", 'name' => $segmentC->getName(), 'link' => "/s/segments/view/{$segmentC->getId()}"],
|
||||
['id' => "{$segmentA->getId()}-{$segmentD->getId()}", 'name' => $segmentD->getName(), 'link' => "/s/segments/view/{$segmentD->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
['id' => "{$segmentC->getId()}-{$segmentE->getId()}", 'name' => $segmentE->getName(), 'link' => "/s/segments/view/{$segmentE->getId()}"],
|
||||
],
|
||||
],
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => "{$segmentE->getId()}-{$segmentA->getId()}",
|
||||
'name' => $segmentA->getName(),
|
||||
'link' => "/s/segments/view/{$segmentA->getId()}",
|
||||
'message' => 'This segment already exists in the segment dependency tree',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'levels' => $responseData['levels'],
|
||||
]
|
||||
);
|
||||
|
||||
$expectedEdges = [
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentB->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentC->getId()}"],
|
||||
['source' => "0-{$segmentA->getId()}", 'target' => "{$segmentA->getId()}-{$segmentD->getId()}"],
|
||||
['source' => "{$segmentA->getId()}-{$segmentC->getId()}", 'target' => "{$segmentC->getId()}-{$segmentE->getId()}"],
|
||||
['source' => "{$segmentC->getId()}-{$segmentE->getId()}", 'target' => "{$segmentE->getId()}-{$segmentA->getId()}"],
|
||||
];
|
||||
|
||||
$actualEdges = $responseData['edges'];
|
||||
Assert::assertCount(count($expectedEdges), $actualEdges, 'Should have the correct number of edges');
|
||||
|
||||
foreach ($expectedEdges as $expectedEdge) {
|
||||
$edgeFound = false;
|
||||
foreach ($actualEdges as $actualEdge) {
|
||||
if ($actualEdge['source'] === $expectedEdge['source'] && $actualEdge['target'] === $expectedEdge['target']) {
|
||||
$edgeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert::assertTrue($edgeFound, "Expected edge {$expectedEdge['source']} -> {$expectedEdge['target']} not found");
|
||||
}
|
||||
}
|
||||
|
||||
public function testRemoveTagFromLeadAction(): void
|
||||
{
|
||||
// Create a lead
|
||||
$lead = $this->createContact('test@email.com');
|
||||
// ... set other properties as needed
|
||||
|
||||
// Create a tag
|
||||
$tag = new Tag();
|
||||
$tag->setTag('Test Tag');
|
||||
// ... set other properties as needed
|
||||
|
||||
// Link the lead and tag
|
||||
$lead->addTag($tag);
|
||||
|
||||
// Persist the lead and tag
|
||||
$this->em->persist($lead);
|
||||
$this->em->persist($tag);
|
||||
$this->em->flush();
|
||||
|
||||
// Call the removeTagFromLeadAction
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=lead:removeTagFromLead', [
|
||||
'leadId' => $lead->getId(),
|
||||
'tagId' => $tag->getId(),
|
||||
]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertTrue($clientResponse->isOk(), $clientResponse->getContent());
|
||||
|
||||
// Assert the tag is removed from the lead
|
||||
$updatedLead = $this->em->getRepository(Lead::class)->find($lead->getId());
|
||||
$this->assertFalse(in_array($tag, $updatedLead->getTags()->toArray()));
|
||||
}
|
||||
|
||||
public function testContactListActionSuggestionsByAdminUser(): void
|
||||
{
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->em->getRepository(User::class);
|
||||
|
||||
/** @var User $adminUser */
|
||||
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
|
||||
self::assertInstanceOf(User::class, $adminUser);
|
||||
|
||||
$salesUser = $userRepository->findOneBy(['username' => 'sales']);
|
||||
self::assertInstanceOf(User::class, $salesUser);
|
||||
|
||||
$leads = [];
|
||||
|
||||
// Create 4 leads with two owned by admin and sales users respectively.
|
||||
for ($i = 1; $i <= 4; ++$i) {
|
||||
$owner = $adminUser;
|
||||
|
||||
if ($i > 2) {
|
||||
$owner = $salesUser;
|
||||
}
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($owner);
|
||||
$leads[] = $lead;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadRepository->saveEntities($leads);
|
||||
$this->em->clear();
|
||||
|
||||
// Check suggestions for admin user.
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:contactList&field=undefined&filter=user');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$foundNames = array_column($data, 'value');
|
||||
|
||||
self::assertCount(4, $foundNames);
|
||||
|
||||
foreach ($foundNames as $key => $name) {
|
||||
self::assertSame('User '.($key + 1), $name);
|
||||
}
|
||||
}
|
||||
|
||||
public function testContactListActionSuggestionsByNonAdminUser(): void
|
||||
{
|
||||
/** @var UserRepository $userRepository */
|
||||
$userRepository = $this->em->getRepository(User::class);
|
||||
|
||||
$adminUser = $userRepository->findOneBy(['username' => 'admin']);
|
||||
self::assertInstanceOf(User::class, $adminUser);
|
||||
|
||||
$leads = [];
|
||||
|
||||
// Create 2 leads with owned by admin user.
|
||||
for ($i = 1; $i <= 2; ++$i) {
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($adminUser);
|
||||
$leads[] = $lead;
|
||||
}
|
||||
|
||||
/** @var LeadRepository $leadRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
$leadRepository->saveEntities($leads);
|
||||
$this->em->clear();
|
||||
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin(false);
|
||||
$role->setRawPermissions(['lead:leads' => ['viewown']]);
|
||||
|
||||
/** @var RoleRepository $roleRepository */
|
||||
$roleRepository = $this->em->getRepository(Role::class);
|
||||
$roleRepository->saveEntity($role);
|
||||
|
||||
// Create a non admin user with view own contacts permission.
|
||||
$user = new User();
|
||||
$user->setFirstName('Non');
|
||||
$user->setLastName('Admin');
|
||||
$user->setEmail('non-admin-user@test.com');
|
||||
$user->setUsername('non-admin-user');
|
||||
$user->setRole($role);
|
||||
|
||||
/** @var PasswordHasherInterface $hasher */
|
||||
$hasher = static::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
|
||||
$passwordNonAdmin = 'Maut1cR0cks!';
|
||||
$user->setPassword($hasher->hash($passwordNonAdmin));
|
||||
$userRepository->saveEntity($user);
|
||||
|
||||
/** @var User $nonAdminUser */
|
||||
$nonAdminUser = $userRepository->findOneBy(['email' => 'non-admin-user@test.com']);
|
||||
|
||||
$nonAdminLeads = [];
|
||||
|
||||
// Create 2 leads with owned by non-admin user.
|
||||
for ($i = 3; $i <= 4; ++$i) {
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname("User $i");
|
||||
$lead->setOwner($nonAdminUser);
|
||||
$nonAdminLeads[] = $lead;
|
||||
}
|
||||
|
||||
$leadRepository->saveEntities($nonAdminLeads);
|
||||
$this->em->clear();
|
||||
|
||||
$this->logoutUser();
|
||||
|
||||
// Check suggestions for a non admin user.
|
||||
$this->client->loginUser($nonAdminUser, 'mautic');
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', 'non-admin-user');
|
||||
// Set the new password, because new authenticator system checks for it.
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', $passwordNonAdmin);
|
||||
$this->client->request(Request::METHOD_GET, '/s/ajax?action=lead:contactList&field=undefined&filter=user');
|
||||
$response = $this->client->getResponse();
|
||||
self::assertTrue($response->isOk());
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$foundNames = array_column($data, 'value');
|
||||
|
||||
self::assertCount(2, $foundNames);
|
||||
self::assertSame('User 3', $foundNames[0]);
|
||||
self::assertSame('User 4', $foundNames[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $expectedOptions
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('leadFieldOrderChoiceListProvider')]
|
||||
public function testUpdateLeadFieldOrderChoiceListAction(string $object, string $group, array $expectedOptions): void
|
||||
{
|
||||
$payload = [
|
||||
'action' => 'lead:updateLeadFieldOrderChoiceList',
|
||||
'object' => $object,
|
||||
'group' => $group,
|
||||
];
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
|
||||
// Get the response HTML
|
||||
$response = $this->client->getResponse();
|
||||
$htmlContent = $response->getContent();
|
||||
|
||||
// Assert the response is successful
|
||||
$this->assertTrue($response->isOk(), "Response was not OK for object: $object, group: $group");
|
||||
$this->assertStringNotContainsString('<form', $htmlContent, 'Response contains a form instead of just field order.');
|
||||
$this->assertStringContainsString('<select', $htmlContent, 'Response contains select tag.');
|
||||
|
||||
// Parse the HTML content using DOMDocument
|
||||
$dom = new \DOMDocument();
|
||||
@$dom->loadHTML($htmlContent);
|
||||
$select = $dom->getElementsByTagName('select')->item(0);
|
||||
$options = $select->getElementsByTagName('option');
|
||||
|
||||
$actualOptions = [];
|
||||
foreach ($options as $option) {
|
||||
if ($option->textContent) {
|
||||
// Get the text content of each <option>
|
||||
$actualOptions[] = trim($option->textContent);
|
||||
}
|
||||
}
|
||||
// Assert that the actual options match the expected options
|
||||
if (empty($expectedOptions)) {
|
||||
$this->assertEmpty($actualOptions);
|
||||
}
|
||||
foreach ($expectedOptions as $expectedValue) {
|
||||
$this->assertContains($expectedValue, $actualOptions, "Missing expected option '$expectedValue' for object: $object, group: $group");
|
||||
}
|
||||
}
|
||||
|
||||
public static function leadFieldOrderChoiceListProvider(): \Generator
|
||||
{
|
||||
yield ['lead', 'core', ['Fax', 'Website']];
|
||||
yield ['lead', 'social', ['Facebook', 'Foursquare', 'Instagram']];
|
||||
yield ['company', 'core', []];
|
||||
}
|
||||
|
||||
public function testTogglePreferredLeadChannelActionWithFlashMessage(): void
|
||||
{
|
||||
$contact = $this->createContact('test@example.com');
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:togglePreferredLeadChannel',
|
||||
'leadId' => $contact->getId(),
|
||||
'channel' => 'email',
|
||||
'channelAction' => 'remove',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:togglePreferredLeadChannel',
|
||||
'leadId' => $contact->getId(),
|
||||
'channel' => 'email',
|
||||
'channelAction' => 'add',
|
||||
];
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
$this->assertTrue(isset($response['flashes']), 'The response should contain flashes');
|
||||
$this->assertNotEmpty($response['flashes'], 'The flashes should not be empty');
|
||||
|
||||
$this->assertStringContainsString('Contact is now contactable on the email channel', $response['flashes'], 'Flash message about channel being contactable should be present');
|
||||
}
|
||||
|
||||
public function testRemoveBounceStatusActionWithFlashMessage(): void
|
||||
{
|
||||
$contact = $this->createContact('bounce@example.com');
|
||||
|
||||
$dnc = new DoNotContact();
|
||||
$dnc->setLead($contact);
|
||||
$dnc->setChannel('email');
|
||||
$dnc->setReason(DoNotContact::BOUNCED);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
|
||||
$this->em->persist($dnc);
|
||||
$this->em->flush();
|
||||
|
||||
$payload = [
|
||||
'action' => 'lead:removeBounceStatus',
|
||||
'id' => $dnc->getId(),
|
||||
'channel' => 'email',
|
||||
];
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest(Request::METHOD_POST, '/s/ajax', $payload);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertTrue(isset($response['success']), 'The response does not contain the `success` param.');
|
||||
$this->assertSame(1, $response['success']);
|
||||
$this->assertTrue(isset($response['flashes']), 'The response should contain flashes');
|
||||
$this->assertNotEmpty($response['flashes'], 'The flashes should not be empty');
|
||||
|
||||
$this->assertStringContainsString('Contact is now contactable on the email channel', $response['flashes'], 'Flash message about channel being contactable should be present');
|
||||
}
|
||||
|
||||
public function testFieldListAction(): void
|
||||
{
|
||||
// Check if alias is null
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => '',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame('Alias cannot be empty', $response['error']);
|
||||
|
||||
// User search for filter
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'owner_id',
|
||||
'filter' => 'Admin',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame('Admin User', $response[0]['value']);
|
||||
|
||||
// Check if filed type is not lookup
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'city',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertEmpty($response);
|
||||
|
||||
// Check if filed type is lookup
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:fieldList',
|
||||
'field' => 'title',
|
||||
'filter' => '',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertArrayHasKey(0, $response);
|
||||
}
|
||||
|
||||
public function testGetLookupChoiceListForGlobalCategory(): void
|
||||
{
|
||||
$category1 = $this->createGlobalCategory('GC11');
|
||||
$category2 = $this->createGlobalCategory('GC12');
|
||||
|
||||
// Search global category with string filter
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:getLookupChoiceList',
|
||||
'for_lookup' => 1,
|
||||
'searchKey' => 'category.category',
|
||||
'category_category' => 'GC11',
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertSame($category1->getTitle(), $response[0]['text']);
|
||||
|
||||
// Search global category with array of ids
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:getLookupChoiceList',
|
||||
'for_lookup' => 1,
|
||||
'searchKey' => 'category.category',
|
||||
'category_category' => [$category1->getId(), $category2->getId()],
|
||||
]
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertCount(2, $response);
|
||||
}
|
||||
|
||||
public function testLoadSegmentFilterFormForSubscribedCategory(): void
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'lead:loadSegmentFilterForm',
|
||||
'fieldAlias' => 'globalcategory',
|
||||
'fieldObject' => 'lead',
|
||||
'operator' => 'in',
|
||||
'filterNum' => 2,
|
||||
]
|
||||
);
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertArrayHasKey('viewParameters', $response);
|
||||
Assert::assertStringContainsString('data-model="category.category"', $response['viewParameters']['form']);
|
||||
}
|
||||
|
||||
private function getMembersForCampaign(int $campaignId): array
|
||||
{
|
||||
return $this->connection->createQueryBuilder()
|
||||
->select('cl.lead_id, cl.manually_added, cl.manually_removed')
|
||||
->from(MAUTIC_TABLE_PREFIX.'campaign_leads', 'cl')
|
||||
->where("cl.campaign_id = {$campaignId}")
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
private function createContact(string $email): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setEmail($email);
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
private function createCampaign(): Campaign
|
||||
{
|
||||
$campaign = new Campaign();
|
||||
|
||||
$campaign->setName('Campaign A');
|
||||
$campaign->setCanvasSettings(
|
||||
[
|
||||
'nodes' => [
|
||||
[
|
||||
'id' => '148',
|
||||
'positionX' => '760',
|
||||
'positionY' => '155',
|
||||
],
|
||||
[
|
||||
'id' => 'lists',
|
||||
'positionX' => '860',
|
||||
'positionY' => '50',
|
||||
],
|
||||
],
|
||||
'connections' => [
|
||||
[
|
||||
'sourceId' => 'lists',
|
||||
'targetId' => '148',
|
||||
'anchors' => [
|
||||
'source' => 'leadsource',
|
||||
'target' => 'top',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
return $campaign;
|
||||
}
|
||||
|
||||
private function createGlobalCategory(string $title): Category
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setIsPublished(true)
|
||||
->setTitle($title)
|
||||
->setAlias(strtolower($title))
|
||||
->setBundle('global');
|
||||
|
||||
$this->em->persist($category);
|
||||
$this->em->flush();
|
||||
|
||||
return $category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CompanyApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @throws \Doctrine\ORM\Exception\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
protected function markCompanyEmailAsUnique(): void
|
||||
{
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
$companyEmailField = $fieldRepository->findOneBy(['alias' => 'companyemail']);
|
||||
\assert($companyEmailField instanceof LeadField);
|
||||
$companyEmailField->setIsUniqueIdentifer(true);
|
||||
$this->em->persist($companyEmailField);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Disable API just for specific test.
|
||||
$this->configParams['api_enabled'] = 'testDisabledApi' !== $this->name();
|
||||
$this->configParams['company_unique_identifiers_operator'] = 'AND';
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testBatchNewEndpoint(): void
|
||||
{
|
||||
$this->markCompanyEmailAsUnique();
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
],
|
||||
[
|
||||
'companyname' => 'BatchUpdate2',
|
||||
],
|
||||
[
|
||||
'companyname' => 'BatchUpdate3',
|
||||
],
|
||||
];
|
||||
|
||||
// create 3 new companies
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert status codes
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0]);
|
||||
$companyId1 = $response['companies'][0]['id'];
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][1]);
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][2]);
|
||||
|
||||
// Assert email
|
||||
$this->assertEquals($payload[0]['companyname'], $response['companies'][0]['fields']['all']['companyname']);
|
||||
$this->assertEquals($payload[1]['companyname'], $response['companies'][1]['fields']['all']['companyname']);
|
||||
$this->assertEquals($payload[2]['companyname'], $response['companies'][2]['fields']['all']['companyname']);
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
],
|
||||
];
|
||||
|
||||
// use unique field to not create new company
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $response['statusCodes'][0]);
|
||||
$this->assertEquals($companyId1, $response['companies'][0]['id']);
|
||||
|
||||
// Assert email
|
||||
$this->assertEquals('BatchUpdate', $response['companies'][0]['fields']['all']['companyname']);
|
||||
|
||||
$payload = [
|
||||
[
|
||||
'companyname' => 'BatchUpdate',
|
||||
'companyemail' => 'BatchUpdate@update.com',
|
||||
],
|
||||
];
|
||||
|
||||
// use both unique fields and create new, because use AND operator between unique fields
|
||||
$this->client->request('POST', '/api/companies/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals(Response::HTTP_CREATED, $response['statusCodes'][0]);
|
||||
$this->assertNotEquals($companyId1, $response['companies'][0]['id']);
|
||||
}
|
||||
|
||||
public function testSingleNewEndpoint(): void
|
||||
{
|
||||
$this->markCompanyEmailAsUnique();
|
||||
|
||||
$payload = [
|
||||
'companyname' => 'API',
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$companyId = $response['company']['id'];
|
||||
|
||||
$this->assertEquals($payload['companyname'], $response['company']['fields']['all']['companyname']);
|
||||
|
||||
// Lets try to create the same company
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertEquals($companyId, $response['company']['id']);
|
||||
|
||||
$payload = [
|
||||
'companyname' => 'API',
|
||||
'companyemail' => 'api@api.com',
|
||||
];
|
||||
|
||||
// Lets try to create the new company because use unique fields with AND operator
|
||||
$this->client->request('POST', '/api/companies/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertNotEquals($companyId, $response['company']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a company via API Platform v2 endpoint.
|
||||
*
|
||||
* @param array<string, mixed> $companyData
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('companyCreateDataProvider')]
|
||||
public function testCreateCompanyViaApiPlatform(array $companyData, int $expectedStatusCode): void
|
||||
{
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/api/v2/companies',
|
||||
[],
|
||||
[],
|
||||
[
|
||||
'CONTENT_TYPE' => 'application/ld+json',
|
||||
'HTTP_ACCEPT' => 'application/ld+json',
|
||||
],
|
||||
json_encode($companyData)
|
||||
);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertSame($expectedStatusCode, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
if (Response::HTTP_CREATED === $expectedStatusCode) {
|
||||
$responseData = json_decode($response->getContent(), true);
|
||||
|
||||
$this->assertIsArray($responseData);
|
||||
$this->assertArrayHasKey('id', $responseData);
|
||||
$this->assertArrayHasKey('score', $responseData);
|
||||
|
||||
// Verify the company was actually created in the database
|
||||
$companyRepository = $this->em->getRepository(\Mautic\LeadBundle\Entity\Company::class);
|
||||
$company = $companyRepository->find($responseData['id']);
|
||||
|
||||
$this->assertInstanceOf(\Mautic\LeadBundle\Entity\Company::class, $company);
|
||||
$this->assertSame($companyData['name'] ?? null, $company->getName());
|
||||
$this->assertSame($companyData['score'] ?? 0, $company->getScore());
|
||||
$this->assertSame($companyData['city'] ?? null, $company->getCity());
|
||||
$this->assertSame($companyData['state'] ?? null, $company->getState());
|
||||
$this->assertSame($companyData['country'] ?? null, $company->getCountry());
|
||||
$this->assertSame($companyData['industry'] ?? null, $company->getIndustry());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{companyData: array<string, mixed>, expectedStatusCode: int}>
|
||||
*/
|
||||
public static function companyCreateDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'valid company with all fields' => [
|
||||
'companyData' => [
|
||||
'score' => 0,
|
||||
'socialCache' => [],
|
||||
'city' => 'Boston',
|
||||
'state' => 'Massachusetts',
|
||||
'country' => 'United States',
|
||||
'name' => 'Mautic',
|
||||
'industry' => 'Software',
|
||||
],
|
||||
'expectedStatusCode' => Response::HTTP_CREATED,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\LeadBundle\Controller\Api\CustomFieldsApiControllerTrait;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class CustomFieldsApiControllerTraitTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetEntityFormOptions(): void
|
||||
{
|
||||
$result = [
|
||||
'field_1' => [
|
||||
'label' => 'Field 1',
|
||||
'type' => 'text',
|
||||
],
|
||||
'field_2' => [
|
||||
'label' => 'Field 2',
|
||||
'type' => 'text',
|
||||
],
|
||||
];
|
||||
|
||||
$paginator = $this->createMock(Paginator::class);
|
||||
$paginator->method('getIterator')
|
||||
->willReturn($result);
|
||||
|
||||
$modelFake = $this->createMock(FieldModel::class);
|
||||
$modelFake->expects(self::once())
|
||||
->method('getEntities')
|
||||
->willReturn($paginator);
|
||||
|
||||
$controller = new class($modelFake) {
|
||||
use CustomFieldsApiControllerTrait;
|
||||
|
||||
private object $model;
|
||||
private string $entityNameOne = 'lead';
|
||||
|
||||
public function __construct(object $modelFake)
|
||||
{
|
||||
$this->model = $modelFake;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getEntityFormOptionsPublic(): array
|
||||
{
|
||||
return $this->getEntityFormOptions();
|
||||
}
|
||||
|
||||
public function getModel(?string $name): object
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
};
|
||||
|
||||
Assert::assertSame($result, (array) $controller->getEntityFormOptionsPublic()['fields']); // Calling once, should be live
|
||||
Assert::assertSame($result, (array) $controller->getEntityFormOptionsPublic()['fields']); // Calling twice, should be cached
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DeviceApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testPutEditWithInexistingIdSoItShouldCreate(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_PUT, '/api/devices/99999/edit', [
|
||||
'device' => 'desktop',
|
||||
'deviceOsName' => 'Ubuntu',
|
||||
'deviceOsShortName' => 'UBT',
|
||||
'deviceOsPlatform' => 'x64',
|
||||
'lead' => $contact->getId(),
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @IgnoreAnnotation("covers")
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(\Mautic\LeadBundle\Controller\Api\FieldApiController::class)]
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(\Mautic\LeadBundle\Field\Command\CreateCustomFieldCommand::class)]
|
||||
final class FieldApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['create_custom_field_in_background'] = 'testFieldApiEndpointsWithBackgroundProcessingEnabled' === $this->name();
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testCreatingMultiselectField(): void
|
||||
{
|
||||
$payload = [
|
||||
'label' => 'Shops (TB)',
|
||||
'alias' => 'shops',
|
||||
'type' => 'multiselect',
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => false,
|
||||
'properties' => [
|
||||
'list' => [
|
||||
['label' => 'label1', 'value' => 'value1'],
|
||||
['label' => 'label2', 'value' => 'value2'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request(Request::METHOD_POST, '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$fieldResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
self::assertResponseStatusCodeSame(Response::HTTP_CREATED, $clientResponse->getContent());
|
||||
Assert::assertTrue($fieldResponse['field']['isPublished']);
|
||||
Assert::assertGreaterThan(0, $fieldResponse['field']['id']);
|
||||
Assert::assertSame($payload['label'], $fieldResponse['field']['label']);
|
||||
Assert::assertSame($payload['alias'], $fieldResponse['field']['alias']);
|
||||
Assert::assertSame($payload['type'], $fieldResponse['field']['type']);
|
||||
Assert::assertSame($payload['isPubliclyUpdatable'], $fieldResponse['field']['isPubliclyUpdatable']);
|
||||
Assert::assertSame($payload['isUniqueIdentifier'], $fieldResponse['field']['isUniqueIdentifier']);
|
||||
Assert::assertSame($payload['properties'], $fieldResponse['field']['properties']);
|
||||
|
||||
// Cleanup
|
||||
$this->client->request(Request::METHOD_DELETE, '/api/fields/contact/'.$fieldResponse['field']['id'].'/delete', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful($clientResponse->getContent());
|
||||
}
|
||||
|
||||
public function testFieldApiEndpointsWithBackgroundProcessingEnabled(): void
|
||||
{
|
||||
$alias = uniqid('field');
|
||||
$payload = $this->getCreatePayload($alias);
|
||||
$id = $this->assertCreateResponse($payload, Response::HTTP_ACCEPTED);
|
||||
|
||||
// Test that the command will create the field
|
||||
$commandTester = $this->testSymfonyCommand('mautic:custom-field:create-column', ['--id' => $id]);
|
||||
|
||||
$this->assertEquals(0, $commandTester->getStatusCode());
|
||||
|
||||
// Test fetching
|
||||
$this->assertGetResponse($payload, $id);
|
||||
|
||||
// Test editing
|
||||
$payload = $this->getEditPayload($id);
|
||||
$this->assertPatchResponse($payload, $id, $alias);
|
||||
|
||||
// Test deleting
|
||||
$this->assertDeleteResponse($payload, $id, $alias, true);
|
||||
}
|
||||
|
||||
public function testFieldApiEndpointsWithBackgroundProcessingDisabled(): void
|
||||
{
|
||||
$alias = uniqid('field');
|
||||
$payload = $this->getCreatePayload($alias);
|
||||
$id = $this->assertCreateResponse($payload, Response::HTTP_CREATED);
|
||||
|
||||
// Test fetching
|
||||
$this->assertGetResponse($payload, $id);
|
||||
|
||||
// Test editing
|
||||
$payload = $this->getEditPayload($id);
|
||||
$this->assertPatchResponse($payload, $id, $alias);
|
||||
|
||||
// Test deleting
|
||||
$this->assertDeleteResponse($payload, $id, $alias, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, string>> $properties
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForCreatingNewBooleanFieldApiEndpoint')]
|
||||
public function testCreatingNewBooleanFieldApiEndpoint(array $properties, string $expectedMessage): void
|
||||
{
|
||||
$payload = [
|
||||
'label' => 'Request a meeting',
|
||||
'alias' => 'meeting',
|
||||
'type' => 'boolean',
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => false,
|
||||
];
|
||||
|
||||
$payload += $properties;
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request(Request::METHOD_POST, '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$errorResponse = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
Assert::assertArrayHasKey('errors', $errorResponse);
|
||||
Assert::assertSame($errorResponse['errors'][0]['code'], $clientResponse->getStatusCode());
|
||||
Assert::assertSame($expectedMessage, $errorResponse['errors'][0]['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|array<string, array<string, string>>>>
|
||||
*/
|
||||
public static function dataForCreatingNewBooleanFieldApiEndpoint(): iterable
|
||||
{
|
||||
yield 'No properties' => [
|
||||
[
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only Yes' => [
|
||||
[
|
||||
'properties'=> [
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
],
|
||||
'A \'negative\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only No' => [
|
||||
[
|
||||
'properties'=> [
|
||||
'no' => 'No',
|
||||
],
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideEmptyMultiSelectValue')]
|
||||
public function testMultiselectSetDefaultValue(mixed $defaultFieldValue): void
|
||||
{
|
||||
$fieldAlias = 'test_multi';
|
||||
|
||||
$fieldModel = $this->getContainer()->get(FieldModel::class);
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
|
||||
$fields = $fieldModel->getLeadFieldCustomFields();
|
||||
Assert::assertEmpty($fields, 'There are no Custom Fields.');
|
||||
|
||||
// Add field.
|
||||
$leadField = new LeadField();
|
||||
$leadField->setName('Test Field')
|
||||
->setAlias($fieldAlias)
|
||||
->setType('multiselect')
|
||||
->setObject('lead')
|
||||
->setProperties([
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'Halusky',
|
||||
'value' => 'halusky',
|
||||
],
|
||||
[
|
||||
'label' => 'Bramborak',
|
||||
'value' => 'bramborak',
|
||||
],
|
||||
[
|
||||
'label' => 'Makovec',
|
||||
'value' => 'makovec',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$fieldModel->saveEntity($leadField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('email@acquia.cz');
|
||||
|
||||
$contact->addUpdatedField($fieldAlias, ['bramborak', 'makovec']);
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Call endpoint
|
||||
$this->client->request('GET', '/api/contacts/'.(string) $contact->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('bramborak|makovec', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => ['halusky'],
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('halusky', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test empty patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => $defaultFieldValue,
|
||||
'overwriteWithBlank' => true,
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Iterator<string, array<string|array<string|null>|null>>
|
||||
*/
|
||||
public static function provideEmptyMultiSelectValue(): \Iterator
|
||||
{
|
||||
yield 'null' => [null];
|
||||
yield 'empty string value' => [''];
|
||||
yield 'empty array with empty string value' => [['']];
|
||||
yield 'empty array with null value' => [[null]];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideEmptySelectValue')]
|
||||
public function testSelectSetDefaultValue(mixed $defaultFieldValue): void
|
||||
{
|
||||
$fieldAlias = 'test_single';
|
||||
|
||||
$fieldModel = $this->getContainer()->get(FieldModel::class);
|
||||
\assert($fieldModel instanceof FieldModel);
|
||||
|
||||
$fields = $fieldModel->getLeadFieldCustomFields();
|
||||
Assert::assertEmpty($fields, 'There are no Custom Fields.');
|
||||
|
||||
// Add field.
|
||||
$leadField = new LeadField();
|
||||
$leadField->setName('Test Field')
|
||||
->setAlias($fieldAlias)
|
||||
->setType('select')
|
||||
->setObject('lead')
|
||||
->setProperties([
|
||||
'list' => [
|
||||
[
|
||||
'label' => 'Halusky',
|
||||
'value' => 'halusky',
|
||||
],
|
||||
[
|
||||
'label' => 'Bramborak',
|
||||
'value' => 'bramborak',
|
||||
],
|
||||
[
|
||||
'label' => 'Makovec',
|
||||
'value' => 'makovec',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$fieldModel->saveEntity($leadField);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('email@acquia.cz');
|
||||
|
||||
$contact->addUpdatedField($fieldAlias, ['makovec']);
|
||||
$contactModel = self::getContainer()->get(LeadModel::class);
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
$contactModel->saveEntity($contact);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Call endpoint
|
||||
$this->client->request('GET', '/api/contacts/'.(string) $contact->getId());
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('makovec', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => 'halusky',
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('halusky', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
|
||||
// Test empty patch and values should be updated
|
||||
$updatedValues = [
|
||||
$fieldAlias => $defaultFieldValue,
|
||||
'overwriteWithBlank' => true,
|
||||
];
|
||||
|
||||
$this->client->request(
|
||||
'PATCH',
|
||||
sprintf('/api/contacts/%d/edit', $contact->getId()),
|
||||
$updatedValues
|
||||
);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$responseJson = \json_decode($clientResponse->getContent(), true, 512, \JSON_THROW_ON_ERROR);
|
||||
self::assertArrayHasKey('contact', $responseJson);
|
||||
self::assertArrayHasKey('fields', $responseJson['contact']);
|
||||
self::assertArrayHasKey('core', $responseJson['contact']['fields']);
|
||||
self::assertArrayHasKey($fieldAlias, $responseJson['contact']['fields']['core']);
|
||||
self::assertArrayHasKey('value', $responseJson['contact']['fields']['core'][$fieldAlias]);
|
||||
self::assertSame('', $responseJson['contact']['fields']['core'][$fieldAlias]['value']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Iterator<string, array<string|null>>
|
||||
*/
|
||||
public static function provideEmptySelectValue(): \Iterator
|
||||
{
|
||||
yield 'null' => [null];
|
||||
yield 'empty string value' => [''];
|
||||
}
|
||||
|
||||
private function assertCreateResponse(array $payload, int $expectedStatusCode): int
|
||||
{
|
||||
// Test creating a new field
|
||||
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request('POST', '/api/fields/contact/new', $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// should be "accepted" if pushed to the background; otherwise "created"
|
||||
$this->assertEquals($expectedStatusCode, $clientResponse->getStatusCode());
|
||||
|
||||
// Assert that the fields returned are what is expected
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
|
||||
if (Response::HTTP_ACCEPTED === $expectedStatusCode && 'isPublished' === $key) {
|
||||
// This should be false because the background job publishes once ready
|
||||
$this->assertEquals(false, $response['field'][$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertEquals($value, $response['field'][$key]);
|
||||
}
|
||||
|
||||
return $response['field']['id'];
|
||||
}
|
||||
|
||||
private function assertGetResponse(array $payload, int $id): void
|
||||
{
|
||||
// Test get and that the field was published
|
||||
$this->client->request('GET', sprintf('/api/fields/contact/%s', $id));
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
// Assert that the fields returned are what is expected and the field is now published
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
$this->assertEquals($value, $response['field'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertPatchResponse(array $payload, int $id, string $alias): void
|
||||
{
|
||||
$typeSafePayload = $this->generateTypeSafePayload($payload);
|
||||
$this->client->request('PATCH', sprintf('/api/fields/contact/%s/edit', $id), $typeSafePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert that the fields returned are what is expected noting that certain fields should not be editable
|
||||
foreach ($payload as $key => $value) {
|
||||
$this->assertTrue(isset($response['field'][$key]));
|
||||
|
||||
match ($key) {
|
||||
'alias' => $this->assertEquals($alias, $response['field'][$key]),
|
||||
'object' => $this->assertEquals('lead', $response['field'][$key]),
|
||||
'type' => $this->assertEquals('text', $response['field'][$key]),
|
||||
default => $this->assertEquals($value, $response['field'][$key]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function assertDeleteResponse(array $payload, int $id, string $alias, bool $isBackground): void
|
||||
{
|
||||
// Test the field is deleted
|
||||
$this->client->request('DELETE', sprintf('/api/fields/contact/%s/delete', $id));
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// Assert that the fields returned are what is expected
|
||||
foreach ($payload as $key => $value) {
|
||||
// use array has key because ID will now be null
|
||||
$this->assertArrayHasKey($key, $response['field']);
|
||||
|
||||
match ($key) {
|
||||
'id' => $isBackground ? $this->assertEquals($value, $response['field'][$key]) : $this->assertNull($response['field'][$key]),
|
||||
'alias' => $this->assertEquals($alias, $response['field'][$key]),
|
||||
'object' => $this->assertEquals('lead', $response['field'][$key]),
|
||||
'type' => $this->assertEquals('text', $response['field'][$key]),
|
||||
default => $this->assertEquals($value, $response['field'][$key]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function getCreatePayload(string $alias): array
|
||||
{
|
||||
return [
|
||||
'isPublished' => true,
|
||||
'label' => 'New Field',
|
||||
'alias' => $alias,
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'lead',
|
||||
'defaultValue' => 'foobar',
|
||||
'isRequired' => true,
|
||||
'isPubliclyUpdatable' => true,
|
||||
'isUniqueIdentifier' => true,
|
||||
'isVisible' => false,
|
||||
'isListable' => false,
|
||||
'isIndex' => true, // Must be true, because if isUniqueIdentifier field is true the contact field *must* be indexed.
|
||||
'charLengthLimit' => 25,
|
||||
'properties' => [],
|
||||
];
|
||||
}
|
||||
|
||||
private function getEditPayload(int $id): array
|
||||
{
|
||||
return [
|
||||
'id' => $id,
|
||||
'label' => 'Foo Bar',
|
||||
'alias' => 'should_not_change',
|
||||
'type' => 'text',
|
||||
'group' => 'core',
|
||||
'object' => 'company',
|
||||
'defaultValue' => 'foobar',
|
||||
'isRequired' => false,
|
||||
'isPubliclyUpdatable' => false,
|
||||
'isUniqueIdentifier' => false,
|
||||
'isVisible' => true,
|
||||
'isListable' => true,
|
||||
'isIndex' => false, // Can be false, if isUniqueIdentifier the field is *not* indexed.
|
||||
'charLengthLimit' => 50,
|
||||
'properties' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\Api\FieldApiController;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\Router;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class FieldApiControllerTest extends TestCase
|
||||
{
|
||||
private $defaultWhere = [
|
||||
[
|
||||
'col' => 'object',
|
||||
'expr' => 'eq',
|
||||
'val' => null,
|
||||
],
|
||||
];
|
||||
|
||||
public function testgetWhereFromRequestWithNoWhere(): void
|
||||
{
|
||||
$request = new Request();
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [$request], $request);
|
||||
|
||||
$this->assertEquals($this->defaultWhere, $result);
|
||||
}
|
||||
|
||||
public function testgetWhereFromRequestWithSomeWhere(): void
|
||||
{
|
||||
$where = [
|
||||
[
|
||||
'col' => 'id',
|
||||
'expr' => 'eq',
|
||||
'val' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
$request = new Request(['where' => $where]);
|
||||
$result = $this->getResultFromProtectedMethod('getWhereFromRequest', [$request], $request);
|
||||
|
||||
$this->assertEquals(array_merge($where, $this->defaultWhere), $result);
|
||||
}
|
||||
|
||||
protected function getResultFromProtectedMethod($method, array $args, Request $request)
|
||||
{
|
||||
$requestStack = $this->createMock(RequestStack::class);
|
||||
$requestStack->method('getCurrentRequest')
|
||||
->willReturn($request);
|
||||
|
||||
$fieldRepository = $this->createMock(LeadFieldRepository::class);
|
||||
$fieldModel = $this->createMock(FieldModel::class);
|
||||
$fieldModel->method('getRepository')
|
||||
->willReturn($fieldRepository);
|
||||
$modelFactory = $this->createMock(ModelFactory::class);
|
||||
$controller = new FieldApiController(
|
||||
$this->createMock(CorePermissions::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(EntityResultHelper::class),
|
||||
$this->createMock(Router::class),
|
||||
$this->createMock(FormFactoryInterface::class),
|
||||
$this->createMock(AppVersion::class),
|
||||
$requestStack,
|
||||
$this->createMock(ManagerRegistry::class),
|
||||
$modelFactory,
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class),
|
||||
$fieldModel,
|
||||
);
|
||||
|
||||
$controllerReflection = new \ReflectionClass(FieldApiController::class);
|
||||
$method = $controllerReflection->getMethod($method);
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invokeArgs($controller, $args);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
|
||||
use Doctrine\Common\Cache\CacheProvider;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Tests that enable debug and profiler to test performance optimizations.
|
||||
* These tests are slower as debug and profiler are enabled. Add tests here only if you need profiler.
|
||||
*/
|
||||
final class LeadApiControllerProfilerTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected array $clientOptions = ['debug' => true];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Disable API just for specific test.
|
||||
$this->configParams['api_enabled'] = true;
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testGetContacts(): void
|
||||
{
|
||||
// reset result cache if any
|
||||
$cache = $this->em->getConfiguration()->getResultCache();
|
||||
|
||||
if ($cache instanceof CacheProvider) {
|
||||
$cache = clone $cache;
|
||||
$cache->setNamespace('leadCount');
|
||||
$cache->deleteAll();
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 11; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setEmail("contact{$i}@email.com");
|
||||
$contact->setPoints($i);
|
||||
$this->em->persist($contact);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->enableProfiler();
|
||||
|
||||
// Make 4 requests to see how many count queries we'll get.
|
||||
$this->getContacts(11);
|
||||
$this->getContacts(11);
|
||||
$this->getContacts(5, ['where' => [['col' => 'l.points', 'expr' => 'lt', 'val' => 5]]]);
|
||||
$this->getContacts(5, ['where' => [['col' => 'l.points', 'expr' => 'lt', 'val' => 5]]]);
|
||||
|
||||
// Without the cache, there would be 4 COUNT queries. With the cache, there is just one.
|
||||
Assert::assertCount(2, $this->findCountQueries());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $queryParams
|
||||
*/
|
||||
private function getContacts(int $expectedCount, array $queryParams = []): void
|
||||
{
|
||||
// We have to reset the param counter to emulate 2 requests otherwise the counter will cause the queries to be different.
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
\assert($leadRepository instanceof LeadRepository);
|
||||
$reflection = new \ReflectionClass($leadRepository);
|
||||
$counter = $reflection->getProperty('lastUsedParameterId');
|
||||
$counter->setAccessible(true);
|
||||
$counter->setValue($leadRepository, 0);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/api/contacts', $queryParams);
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$response = json_decode($this->client->getResponse()->getContent(), true);
|
||||
Assert::assertSame($expectedCount, (int) $response['total']);
|
||||
Assert::assertCount($expectedCount, $response['contacts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed[]>
|
||||
*/
|
||||
private function findCountQueries(): array
|
||||
{
|
||||
/** @var DoctrineDataCollector $dbCollector */
|
||||
$dbCollector = $this->client->getProfile()->getCollector('db');
|
||||
$allQueries = $dbCollector->getQueries()['default'];
|
||||
|
||||
return array_filter(
|
||||
$allQueries,
|
||||
fn (array $query) => str_starts_with($query['sql'], 'SELECT COUNT(l.id) as count FROM '.MAUTIC_TABLE_PREFIX.'leads l')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CampaignBundle\Entity\Campaign;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ListApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected ListModel $listModel;
|
||||
|
||||
private string $prefix;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->listModel = static::getContainer()->get('mautic.lead.model.list');
|
||||
$this->prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$this->translator = static::getContainer()->get('translator');
|
||||
}
|
||||
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement(['categories']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<string|int|null>>
|
||||
*/
|
||||
public static function regexOperatorProvider(): iterable
|
||||
{
|
||||
yield [
|
||||
'regexp',
|
||||
'^{Test|Test string)', // invalid regex: the first parantheses should not be curly
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
'error',
|
||||
];
|
||||
|
||||
yield [
|
||||
'!regexp',
|
||||
'^(Test|Test string))', // invalid regex: 2 ending parantheses
|
||||
Response::HTTP_BAD_REQUEST,
|
||||
'error',
|
||||
];
|
||||
|
||||
yield [
|
||||
'regexp',
|
||||
'^(Test|Test string)', // valid regex
|
||||
Response::HTTP_CREATED,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('regexOperatorProvider')]
|
||||
public function testRegexOperatorValidation(string $operator, string $regex, int $expectedResponseCode, ?string $expectedErrorMessage): void
|
||||
{
|
||||
$this->client->request(
|
||||
Request::METHOD_POST,
|
||||
'/api/segments/new',
|
||||
[
|
||||
'name' => 'Regex test',
|
||||
'filters' => [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => $operator,
|
||||
'properties' => ['filter' => $regex],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
Assert::assertSame($expectedResponseCode, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
if ($expectedErrorMessage) {
|
||||
Assert::assertStringContainsString(
|
||||
$expectedErrorMessage,
|
||||
json_decode($this->client->getResponse()->getContent(), true)['errors'][0]['message'],
|
||||
$this->client->getResponse()->getContent()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSingleSegmentWorkflow(): void
|
||||
{
|
||||
$payload = [
|
||||
'name' => 'API segment',
|
||||
'description' => 'Segment created via API test',
|
||||
'filters' => [
|
||||
// Legacy structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
// Current structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'display' => 'outdated name',
|
||||
'filter' => 'outdated_id',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Create:
|
||||
$this->client->request('POST', '/api/segments/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response['errors'][0])) {
|
||||
$this->fail($response['errors'][0]['code'].': '.$response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
$segmentId = $response['list']['id'];
|
||||
|
||||
$this->assertSame(201, $clientResponse->getStatusCode());
|
||||
$this->assertGreaterThan(0, $segmentId);
|
||||
$this->assertEquals($payload['name'], $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
$this->assertEquals([
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'owner_id',
|
||||
'type' => 'lookup_id',
|
||||
'operator' => '=',
|
||||
'properties' => [
|
||||
'display' => 'John Doe',
|
||||
'filter' => '4',
|
||||
],
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'properties' => [
|
||||
'filter' => null,
|
||||
],
|
||||
],
|
||||
],
|
||||
$response['list']['filters']
|
||||
);
|
||||
|
||||
// Edit:
|
||||
$this->client->request('PATCH', "/api/segments/{$segmentId}/edit", ['name' => 'API segment renamed']);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertSame($segmentId, $response['list']['id'], 'ID of the created segment does not match with the edited one.');
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Get:
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertSame($segmentId, $response['list']['id'], 'ID of the created segment does not match with the fetched one.');
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Delete:
|
||||
$this->client->request('DELETE', "/api/segments/{$segmentId}/delete");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(200, $clientResponse->getStatusCode());
|
||||
$this->assertNull($response['list']['id']);
|
||||
$this->assertEquals('API segment renamed', $response['list']['name']);
|
||||
$this->assertEquals($payload['description'], $response['list']['description']);
|
||||
|
||||
// Get (ensure it's deleted):
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(404, $clientResponse->getStatusCode());
|
||||
$this->assertSame(404, $response['errors'][0]['code']);
|
||||
}
|
||||
|
||||
public function testBatchSegmentWorkflow(): void
|
||||
{
|
||||
$payload = [
|
||||
[
|
||||
'name' => 'API batch segment 1',
|
||||
'description' => 'Segment created via API test',
|
||||
'filters' => [
|
||||
// Legacy structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
// Current structure.
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'API batch segment 2',
|
||||
'description' => 'Segment created via API test',
|
||||
],
|
||||
];
|
||||
|
||||
$this->client->request('POST', '/api/segments/batch/new', $payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response1 = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response1['errors'][0])) {
|
||||
$this->fail($response1['errors'][0]['code'].': '.$response1['errors'][0]['message']);
|
||||
}
|
||||
|
||||
foreach ($response1['statusCodes'] as $statusCode) {
|
||||
$this->assertSame(201, $statusCode);
|
||||
}
|
||||
|
||||
foreach ($response1['lists'] as $key => $segment) {
|
||||
$this->assertGreaterThan(0, $segment['id']);
|
||||
$this->assertTrue($segment['isPublished']);
|
||||
$this->assertTrue($segment['isGlobal']);
|
||||
$this->assertFalse($segment['isPreferenceCenter']);
|
||||
$this->assertSame($payload[$key]['name'], $segment['name']);
|
||||
$this->assertSame($payload[$key]['description'], $segment['description']);
|
||||
$this->assertIsArray($segment['filters']);
|
||||
|
||||
// Make a change for the edit request:
|
||||
$response1['lists'][$key]['isPublished'] = false;
|
||||
}
|
||||
|
||||
// Lets try to create the same segment to see that the values are not re-setted
|
||||
$this->client->request('PATCH', '/api/segments/batch/edit', $response1['lists']);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response2 = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
if (!empty($response2['errors'][0])) {
|
||||
$this->fail($response2['errors'][0]['code'].': '.$response2['errors'][0]['message']);
|
||||
}
|
||||
|
||||
foreach ($response2['statusCodes'] as $statusCode) {
|
||||
$this->assertSame(200, $statusCode);
|
||||
}
|
||||
|
||||
foreach ($response2['lists'] as $key => $segment) {
|
||||
$this->assertGreaterThan(0, $segment['id']);
|
||||
$this->assertFalse($segment['isPublished']);
|
||||
$this->assertTrue($segment['isGlobal']);
|
||||
$this->assertFalse($segment['isPreferenceCenter']);
|
||||
$this->assertSame($payload[$key]['name'], $segment['name']);
|
||||
$this->assertSame($payload[$key]['description'], $segment['description']);
|
||||
}
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'city',
|
||||
'type' => 'text',
|
||||
'operator' => '=',
|
||||
'properties' => ['filter' => 'Prague'],
|
||||
'filter' => 'Prague',
|
||||
'display' => null,
|
||||
],
|
||||
],
|
||||
$response2['lists'][0]['filters']
|
||||
);
|
||||
|
||||
$this->assertSame([], $response2['lists'][1]['filters']);
|
||||
}
|
||||
|
||||
public function testWeGet422ResponseCodeIfSegmentIsBeingUsedInSomeCampaignAndWeUnpublishIt(): void
|
||||
{
|
||||
$segmentName = 'Segment1';
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(mb_strtolower($segmentName));
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaignName = 'Campaign1';
|
||||
$campaign->setName($campaignName);
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
// insert unpublished record
|
||||
$this->connection->insert($this->prefix.'campaign_leadlist_xref', [
|
||||
'campaign_id' => $campaign->getId(),
|
||||
'leadlist_id' => $segment->getId(),
|
||||
]);
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$segment->getId()}/edit", ['isPublished' => 0]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $clientResponse->getStatusCode());
|
||||
Assert::assertArrayHasKey('errors', $response);
|
||||
$errorMessage = $this->translator->trans(
|
||||
'mautic.lead.lists.used_in_campaigns',
|
||||
[
|
||||
'%count%' => '1',
|
||||
'%campaignNames%' => '"'.$campaignName.'"',
|
||||
],
|
||||
'validators'
|
||||
);
|
||||
Assert::assertStringContainsString($errorMessage, $response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
public function testWeGet200ResponseCodeIfSegmentIsNotUsedInCampaignsAndWeUnpublishIt(): void
|
||||
{
|
||||
$segmentName = 'Segment1';
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(mb_strtolower($segmentName));
|
||||
$segment->setIsPublished(true);
|
||||
$this->em->persist($segment);
|
||||
|
||||
$campaign = new Campaign();
|
||||
$campaign->setName('campaign1');
|
||||
|
||||
$this->em->persist($campaign);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$segment->getId()}/edit", ['isPublished' => 0]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
Assert::assertSame(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
Assert::assertArrayNotHasKey('errors', $response);
|
||||
}
|
||||
|
||||
public function testUnpublishUsedSingleSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('leadlist: This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$this->client->request('PATCH', "/api/segments/{$list1->getId()}/edit", ['name' => 'API segment renamed', 'isPublished' => false]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $clientResponse->getStatusCode());
|
||||
$this->assertSame($response['errors'][0]['message'], $expectedErrorMessage);
|
||||
}
|
||||
|
||||
public function testUnpublishUsedBatchSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('leadlist: This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$segments = [
|
||||
['id' => $list1->getId(), 'isPublished' => false],
|
||||
['id' => $list2->getId(), 'isPublished' => false],
|
||||
];
|
||||
|
||||
$this->client->request('PATCH', '/api/segments/batch/edit', $segments);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response['statusCodes'][0]);
|
||||
$this->assertSame($response['errors'][0]['message'], $expectedErrorMessage);
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $response['statusCodes'][1]);
|
||||
}
|
||||
|
||||
public function testSegmentWithCategory(): void
|
||||
{
|
||||
$categoryPayload = [
|
||||
'title' => 'API Cat',
|
||||
'alias' => 'kitty',
|
||||
'bundle' => 'segment',
|
||||
];
|
||||
$this->client->request('POST', '/api/categories/new', $categoryPayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$categoryId = $response['category']['id'];
|
||||
|
||||
$segmentPayload = [
|
||||
'name' => 'API segment',
|
||||
'description' => 'Segment created via API test',
|
||||
'category' => $categoryId,
|
||||
];
|
||||
|
||||
// Create:
|
||||
$this->client->request('POST', '/api/segments/new', $segmentPayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
if (!empty($response['errors'][0])) {
|
||||
$this->fail($response['errors'][0]['code'].': '.$response['errors'][0]['message']);
|
||||
}
|
||||
|
||||
$segmentId = $response['list']['id'];
|
||||
|
||||
// Get segment with category by id:
|
||||
$this->client->request('GET', "/api/segments/{$segmentId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertTrue($clientResponse->isOk());
|
||||
$this->assertEquals($segmentPayload['category'], $response['list']['category']['id']);
|
||||
|
||||
// Search segments by category:
|
||||
$this->client->request('GET', '/api/segments?search=category:kitty');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertTrue($clientResponse->isOk());
|
||||
$this->assertCount(1, $response['lists']);
|
||||
}
|
||||
|
||||
private function saveSegment(string $name, string $alias, array $filters = [], ?LeadList $segment = null): LeadList
|
||||
{
|
||||
$segment ??= new LeadList();
|
||||
$segment->setName($name)->setPublicName($name)->setAlias($alias)->setFilters($filters);
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller\Api;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TagApiControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testTagWorkflow(): void
|
||||
{
|
||||
$tag1Payload = ['tag' => 'test_tag'];
|
||||
|
||||
// Create new tag
|
||||
$this->client->request('POST', '/api/tags/new', $tag1Payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$tagId = $response['tag']['id'];
|
||||
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, 'Return code must be 201.');
|
||||
$this->assertGreaterThan(0, $tagId);
|
||||
$this->assertEquals($tag1Payload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Try to create tag with same name
|
||||
$this->client->request('POST', '/api/tags/new', $tag1Payload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
// The same tag id should be returned
|
||||
$this->assertEquals($tagId, $response['tag']['id'], 'ID of created tag with the same name does not match. Possible duplicates.');
|
||||
$this->assertEquals($tag1Payload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Edit tag name
|
||||
$tag1RenamePayload = ['tag' => 'tag_renamed'];
|
||||
$this->client->request('PATCH', "/api/tags/{$tagId}/edit", $tag1RenamePayload);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of the created tag does not match with the edited one.');
|
||||
$this->assertEquals($tag1RenamePayload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Get tag
|
||||
$this->client->request('GET', "/api/tags/{$tagId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of the created tag does not match with the fetched one.');
|
||||
$this->assertEquals($tag1RenamePayload['tag'], $response['tag']['tag']);
|
||||
|
||||
// Delete:
|
||||
$this->client->request('DELETE', "/api/tags/{$tagId}/delete");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful($clientResponse->getContent());
|
||||
|
||||
// Get (ensure it's deleted):
|
||||
$this->client->request('GET', "/api/tags/{$tagId}");
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
$this->assertSame(404, $response['errors'][0]['code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if whitespace before or after tag name is removed and duplicates are not created.
|
||||
*/
|
||||
public function testWhitespaceBeforeAndAfterNameNotCreatingDuplicates(): void
|
||||
{
|
||||
$tagName = 'test';
|
||||
$whitespaceTestPayload = ['test', 'test ', ' test', "test\t", "\ttest"];
|
||||
$tagId = null;
|
||||
foreach ($whitespaceTestPayload as $payload) {
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $payload]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
|
||||
// whitespace before and after tag name should be removed, name should be the same for each tag
|
||||
$this->assertEquals($tagName, $response['tag']['tag']);
|
||||
if (null === $tagId) {
|
||||
$tagId = $response['tag']['id'];
|
||||
} else {
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of created tag does not match. Possible duplicates.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if special characters in tag name are encoded and duplicates are not created.
|
||||
*/
|
||||
public function testEncodedCharactersNotCreatingDuplicates(): void
|
||||
{
|
||||
$tagInputName = 'hello" world';
|
||||
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $tagInputName]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$tagId = $response['tag']['id'];
|
||||
$this->assertGreaterThan(0, $tagId);
|
||||
$this->assertEquals($tagInputName, $response['tag']['tag']);
|
||||
|
||||
// Try to create duplicate
|
||||
$this->client->request('POST', '/api/tags/new', ['tag' => $tagInputName]);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$response = json_decode($clientResponse->getContent(), true);
|
||||
$this->assertSame($tagId, $response['tag']['id'], 'ID of created tag does not match. Possible duplicates.');
|
||||
}
|
||||
|
||||
public function testTagCreationWithoutRequiredData(): void
|
||||
{
|
||||
// Sending an empty payload should return a 500 server error
|
||||
// TODO ensure that the server sends back a 400 status code instead
|
||||
$this->client->request('POST', '/api/tags/new', []);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertResponseStatusCodeSame(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class AuditLogControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
private const SALES_USER = 'sales';
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testBatchExportActionAsAdmin(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/auditlog/batchExport/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchExportActionAsUserNotPermission(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => self::SALES_USER]);
|
||||
$this->loginUser($user);
|
||||
$this->client->request('GET', '/s/contacts/auditlog/batchExport/'.$contact->getId());
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CompanyControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
private int $company1Id;
|
||||
|
||||
private int $company2Id;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$companiesData = [
|
||||
1 => [
|
||||
'name' => 'Amazon',
|
||||
'state' => 'Washington',
|
||||
'city' => 'Seattle',
|
||||
'country' => 'United States',
|
||||
'industry' => 'Goods',
|
||||
],
|
||||
2 => [
|
||||
'name' => 'Google',
|
||||
'state' => 'Washington',
|
||||
'city' => 'Seattle',
|
||||
'country' => 'United States',
|
||||
'industry' => 'Services',
|
||||
],
|
||||
];
|
||||
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $model */
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
|
||||
foreach ($companiesData as $i => $companyData) {
|
||||
$company = new Company();
|
||||
$company->setIsPublished(true)
|
||||
->setName($companyData['name'])
|
||||
->setState($companyData['state'])
|
||||
->setCity($companyData['city'])
|
||||
->setCountry($companyData['country'])
|
||||
->setIndustry($companyData['industry']);
|
||||
$model->saveEntity($company);
|
||||
|
||||
$this->{'company'.$i.'Id'} = $company->getId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's view page.
|
||||
*/
|
||||
public function testViewActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/view/'.$this->company1Id);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
$company = $model->getEntity($this->company1Id);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$this->assertStringContainsString($company->getName(), $clientResponseContent, 'The return must contain the name of company');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's edit page.
|
||||
*/
|
||||
public function testEditActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/edit/'.$this->company1Id);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$model = self::getContainer()->get('mautic.lead.model.company');
|
||||
$company = $model->getEntity($this->company1Id);
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Company '.$company->getName(), $clientResponseContent, 'The return must contain \'Edit Company\' text');
|
||||
}
|
||||
|
||||
/* Get company contacts list */
|
||||
public function testListCompanyContacts(): void
|
||||
{
|
||||
/** @var \Mautic\LeadBundle\Model\CompanyModel $companyModel */
|
||||
$companyModel = self::getContainer()->get('mautic.lead.model.company');
|
||||
$leadModel = self::getContainer()->get('mautic.lead.model.lead');
|
||||
$company1 = $companyModel->getEntity($this->company1Id);
|
||||
|
||||
// Create a lead linked to the first company
|
||||
$lead1 = new Lead();
|
||||
$lead1
|
||||
->setFirstname('lead')
|
||||
->setLastname('for '.$company1->getName());
|
||||
$leadModel->saveEntity($lead1);
|
||||
|
||||
$companyModel->addLeadToCompany($company1, $lead1);
|
||||
|
||||
// Create a lead not linked to a company
|
||||
$lead2 = new Lead();
|
||||
$lead2
|
||||
->setFirstname('lead')
|
||||
->setLastname('without company');
|
||||
$leadModel->saveEntity($lead2);
|
||||
|
||||
// Create a lead not linked to a company, but with `ids` in it's name (see https://github.com/mautic/mautic/issues/12415)
|
||||
$lead3 = new Lead();
|
||||
$lead3
|
||||
->setFirstname('lead')
|
||||
->setLastname('without company')
|
||||
->setEmail('example@idstart.com');
|
||||
$leadModel->saveEntity($lead3);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/company/'.$this->company1Id.'/contacts/');
|
||||
$leadsTableRows = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertEquals(1, $leadsTableRows->count(), $crawler->html());
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/company/'.$this->company2Id.'/contacts/');
|
||||
$leadsTableRows = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertEquals(0, $leadsTableRows->count(), $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get company's create page.
|
||||
*/
|
||||
public function testNewActionCompany(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
}
|
||||
|
||||
public function testNonExitingCompanyIsRedirected(): void
|
||||
{
|
||||
$this->client->followRedirects(false);
|
||||
$this->client->request(
|
||||
Request::METHOD_GET,
|
||||
's/companies/view/1000',
|
||||
);
|
||||
$this->assertEquals(true, $this->client->getResponse()->isRedirect('/s/companies'));
|
||||
}
|
||||
|
||||
public function testNewCompanyMergeButtonVisible(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/companies/new/');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$clientResponseContent = $clientResponse->getContent();
|
||||
$this->assertEquals(Response::HTTP_OK, $clientResponse->getStatusCode());
|
||||
|
||||
// Use the Crawler to parse the HTML content
|
||||
$crawler = new \Symfony\Component\DomCrawler\Crawler($clientResponseContent);
|
||||
|
||||
// Check for specific buttons by their IDs
|
||||
$applyButton = $crawler->filter('#company_buttons_apply');
|
||||
$saveButton = $crawler->filter('#company_buttons_save');
|
||||
$cancelButton = $crawler->filter('#company_buttons_cancel');
|
||||
$mergeButton = $crawler->filter('#company_buttons_merge');
|
||||
|
||||
$this->assertCount(1, $applyButton, 'Apply button not found');
|
||||
$this->assertCount(1, $saveButton, 'Save button not found');
|
||||
$this->assertCount(1, $cancelButton, 'Cancel button not found');
|
||||
$this->assertCount(0, $mergeButton, 'Merge button found');
|
||||
}
|
||||
|
||||
public function testCompanyWithProject(): void
|
||||
{
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/companies/edit/'.$this->company1Id);
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['company[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedCompany = $this->em->find(Company::class, $this->company1Id);
|
||||
$this->assertSame($project->getId(), $savedCompany->getProjects()->first()->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class CompanyProjectSearchFunctionalTest 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');
|
||||
|
||||
$companyAlpha = $this->createCompany('Company Alpha');
|
||||
$companyBeta = $this->createCompany('Company Beta');
|
||||
$this->createCompany('Company Gamma');
|
||||
$this->createCompany('Company Delta');
|
||||
|
||||
$companyAlpha->addProject($projectOne);
|
||||
$companyAlpha->addProject($projectTwo);
|
||||
$companyBeta->addProject($projectTwo);
|
||||
$companyBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/companies', '/s/companies']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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' => ['Company Alpha', 'Company Beta'],
|
||||
'unexpectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND company name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Company Beta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR company name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Beta', 'Company Gamma'],
|
||||
'unexpectedEntities' => ['Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Company Beta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Beta'],
|
||||
'unexpectedEntities' => ['Company Gamma', 'Company Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Company Alpha', 'Company Gamma', 'Company Delta'],
|
||||
'unexpectedEntities' => ['Company Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createCompany(string $name): Company
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName($name);
|
||||
$this->em->persist($company);
|
||||
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class FieldControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testLengthValidationOnLabelFieldWhenAddingCustomFieldFailure(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$label = 'The leading Drupal Cloud platform to securely develop, deliver, and run websites, applications, and content. Top-of-the-line hosting options are paired with automated testing and development tools. Documentation is also included for the following components';
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$labelErrorMessage = trim($crawler->filter('#leadfield_label')->nextAll()->text());
|
||||
$maxLengthErrorMessageTemplate = 'Label value cannot be longer than 191 characters';
|
||||
|
||||
$this->assertEquals($maxLengthErrorMessageTemplate, $labelErrorMessage);
|
||||
}
|
||||
|
||||
public function testLengthValidationOnLabelFieldWhenAddingCustomFieldSuccess(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$label = 'Test value for custom field 4';
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
}
|
||||
|
||||
public function testCloneFieldSubmission(): void
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setLabel('Field to be cloned');
|
||||
$field->setAlias('field_to_be_cloned');
|
||||
$field->setType('text');
|
||||
|
||||
$this->em->getRepository(LeadField::class)->saveEntity($field);
|
||||
$this->em->clear();
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['alias' => 'field_to_be_cloned']);
|
||||
$this->assertNotNull($field);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/clone/'.$field->getId());
|
||||
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
$this->assertSelectorTextContains('h1', 'New Custom Field');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue('Cloned Field');
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
|
||||
$clonedField = $this->em->getRepository(LeadField::class)->findOneBy(['label' => 'Cloned Field']);
|
||||
$this->assertNotNull($clonedField);
|
||||
$this->assertNotEquals($field->getId(), $clonedField->getId());
|
||||
}
|
||||
|
||||
public function testCloneNonExistentField(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/contacts/fields/clone/9999');
|
||||
$this->assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getStringTypeFieldsArray')]
|
||||
public function testMaxCharLengthFieldValidationOnStringTypeWhenAddingCustomFieldFailure(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$form['leadfield[charLengthLimit]']->setValue('260');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$errorMessage = trim($crawler->filter('#leadfield_charLengthLimit')->nextAll()->text());
|
||||
$maxCharLimitErrorMessage = 'This value should be between 1 and 191.';
|
||||
|
||||
$this->assertEquals($maxCharLimitErrorMessage, $errorMessage);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getStringTypeFieldsArray')]
|
||||
public function testMaxCharLengthFieldValidationOnStringTypeWhenAddingCustomFieldSuccess(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$form['leadfield[charLengthLimit]']->setValue('191');
|
||||
$this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
public static function getStringTypeFieldsArray(): iterable
|
||||
{
|
||||
yield ['test_email', 'email'];
|
||||
yield ['test_text', 'text'];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('getCustomFields')]
|
||||
public function testCustomFieldCharacterLengthLimit(string $label, string $type): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/fields/new');
|
||||
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form['leadfield[label]']->setValue($label);
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[type]']->setValue($type);
|
||||
$this->client->submit($form);
|
||||
|
||||
$field = $this->em->getRepository(LeadField::class)->findOneBy(['label' => $label]);
|
||||
$this->assertNotNull($field);
|
||||
|
||||
/** @var ColumnSchemaHelper $helper */
|
||||
$helper = $this->getContainer()->get('mautic.schema.helper.column');
|
||||
|
||||
// Table name to check the fields.
|
||||
$name = 'leads';
|
||||
$schemaHelper = $helper->setName($name);
|
||||
|
||||
/** @var Column $fieldsDescription */
|
||||
$fieldsDescription = $schemaHelper->getColumns()[$label];
|
||||
|
||||
$this->assertSame(191, $fieldsDescription->getLength());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed, mixed>
|
||||
*/
|
||||
public static function getCustomFields(): iterable
|
||||
{
|
||||
yield ['test_timezone', 'timezone'];
|
||||
yield ['test_locale', 'locale'];
|
||||
yield ['test_country', 'country'];
|
||||
yield ['test_phone', 'tel'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Field\InputFormField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class FieldFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideFieldLength')]
|
||||
public function testNewFieldVarcharFieldLength(int $expectedLength, ?int $inputLength = null): void
|
||||
{
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$field = $this->createField('a', 'text', [], $inputLength);
|
||||
$fieldModel->saveEntity($field);
|
||||
|
||||
$tablePrefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$columns = $this->connection->createSchemaManager()->listTableColumns("{$tablePrefix}leads");
|
||||
$this->assertEquals($expectedLength, $columns[$field->getAlias()]->getLength());
|
||||
}
|
||||
|
||||
public function testNewMultiSelectField(): void
|
||||
{
|
||||
$fieldModel = static::getContainer()->get('mautic.lead.model.field');
|
||||
$field = $this->createField('s', 'select', ['properties' => ['list' => ['choice_a' => 'Choice A']]]);
|
||||
$fieldModel->saveEntity($field);
|
||||
|
||||
$tablePrefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$columns = $this->connection->createSchemaManager()->listTableColumns("{$tablePrefix}leads");
|
||||
$this->assertArrayHasKey('field_s', $columns);
|
||||
}
|
||||
|
||||
public function testNewDateField(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
|
||||
$form['leadfield[label]']->setValue('Best Date Ever');
|
||||
$form['leadfield[type]']->setValue('date');
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $text);
|
||||
Assert::assertStringNotContainsString('New Custom Field', $text);
|
||||
Assert::assertStringNotContainsString('This form should not contain extra fields.', $text);
|
||||
Assert::assertStringContainsString('Edit Custom Field - Best Date Ever', $text);
|
||||
}
|
||||
|
||||
public function testNewSelectField(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$inputLabel = $domDocument->createElement('input');
|
||||
$inputLabel->setAttribute('type', 'text');
|
||||
|
||||
$inputLabel->setAttribute('name', 'leadfield[properties][list][0][label]');
|
||||
$inputValue = $domDocument->createElement('input');
|
||||
$inputValue->setAttribute('type', 'text');
|
||||
$inputValue->setAttribute('name', 'leadfield[properties][list][0][value]');
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form->set(new InputFormField($inputLabel));
|
||||
$form->set(new InputFormField($inputValue));
|
||||
|
||||
$form['leadfield[label]']->setValue('Test select field');
|
||||
$form['leadfield[type]']->setValue('select');
|
||||
$form['leadfield[properties][list][0][label]']->setValue('Label 1');
|
||||
$form['leadfield[properties][list][0][value]']->setValue('Value 1');
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $text);
|
||||
Assert::assertStringNotContainsString('New Custom Field', $text);
|
||||
Assert::assertStringNotContainsString('This form should not contain extra fields.', $text);
|
||||
Assert::assertStringContainsString('Edit Custom Field - Test select field', $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $properties
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForCreatingNewBooleanField')]
|
||||
public function testCreatingNewBooleanField(array $properties, string $expectedMessage): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$yesLabel = $domDocument->createElement('input');
|
||||
$yesLabel->setAttribute('type', 'text');
|
||||
$yesLabel->setAttribute('name', 'leadfield[properties][yes]');
|
||||
|
||||
$noLabel = $domDocument->createElement('input');
|
||||
$noLabel->setAttribute('type', 'text');
|
||||
$noLabel->setAttribute('name', 'leadfield[properties][no]');
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form->set(new InputFormField($yesLabel));
|
||||
$form->set(new InputFormField($noLabel));
|
||||
|
||||
$form['leadfield[label]']->setValue('Request a meeting');
|
||||
$form['leadfield[type]']->setValue('boolean');
|
||||
$form['leadfield[object]']->setValue('lead');
|
||||
$form['leadfield[group]']->setValue('core');
|
||||
|
||||
$form['leadfield[properties][yes]']->setValue($properties['yes'] ?? '');
|
||||
$form['leadfield[properties][no]']->setValue($properties['no'] ?? '');
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$text = strip_tags($this->client->getResponse()->getContent());
|
||||
Assert::assertStringNotContainsString($expectedMessage, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|array<string, string>>>
|
||||
*/
|
||||
public static function dataForCreatingNewBooleanField(): iterable
|
||||
{
|
||||
yield 'No properties' => [
|
||||
[],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only Yes' => [
|
||||
[
|
||||
'yes' => 'Yes',
|
||||
],
|
||||
'A \'negative\' label is required.',
|
||||
];
|
||||
|
||||
yield 'Only No' => [
|
||||
[
|
||||
'no' => 'No',
|
||||
],
|
||||
'A \'positive\' label is required.',
|
||||
];
|
||||
}
|
||||
|
||||
public function testCheckDefaultBooleanFieldSetting(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/contacts/fields/new');
|
||||
|
||||
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
|
||||
|
||||
// Check if the radio button with value 0 is checked and value 1 is not
|
||||
Assert::assertNotNull(
|
||||
$crawler->filter('#leadfield_default_template_boolean_0')->attr('checked')
|
||||
);
|
||||
Assert::assertNull(
|
||||
$crawler->filter('#leadfield_default_template_boolean_1')->attr('checked')
|
||||
);
|
||||
}
|
||||
|
||||
public function testFieldsSearchByIds(): void
|
||||
{
|
||||
$urlEncodedSearch = urlencode('ids:2,3');
|
||||
$this->client->request(Request::METHOD_GET, "/s/contacts/fields?search={$urlEncodedSearch}");
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertStringContainsString('First Name', $this->client->getResponse()->getContent());
|
||||
Assert::assertStringContainsString('Last Name', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
private function createField(string $suffix, string $type = 'text', array $parameters = [], ?int $charLength = null): LeadField
|
||||
{
|
||||
$field = new LeadField();
|
||||
$field->setName("Field $suffix");
|
||||
$field->setAlias("field_$suffix");
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateAdded(new \DateTime());
|
||||
$field->setDateModified(new \DateTime());
|
||||
$field->setType($type);
|
||||
if (!empty($charLength)) {
|
||||
$field->setCharLengthLimit($charLength);
|
||||
}
|
||||
$field->setObject('lead');
|
||||
isset($parameters['properties']) && $field->setProperties($parameters['properties']);
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<array<mixed>>
|
||||
*/
|
||||
public static function provideFieldLength(): iterable
|
||||
{
|
||||
yield [ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH, ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH];
|
||||
yield [64, null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\ImportCommand;
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\CompanyLead;
|
||||
use Mautic\LeadBundle\Entity\Import;
|
||||
use Mautic\LeadBundle\Entity\ImportRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\DomCrawler\Form;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
final class ImportControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
public function testImportWithoutFile(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$form = $crawler->selectButton('Upload')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
Assert::assertStringContainsString('Please select a CSV file to upload', $crawler->html(), $crawler->html());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting the phone field as required to test the validation.
|
||||
* Phone is not part of the csv fixture so it won't be auto-mapped.
|
||||
*/
|
||||
public function testImportMappingRequiredFieldValidation(): void
|
||||
{
|
||||
$this->setPhoneFieldIsRequired(true);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(__DIR__.'/../Fixtures/contacts.csv', 'contacs.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Some required fields are missing. You must map the field "Phone."', $crawler->html());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('validateDataProvider')]
|
||||
public function testImportMappingAndImport(string $skipIfExist, string $expectedName): void
|
||||
{
|
||||
$this->createLead('john@doe.email', 'Johny');
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(__DIR__.'/../Fixtures/contacts.csv', 'contacs.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$mappingForm['lead_field_import[skip_if_exists]']->setValue($skipIfExist);
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created. You will be notified when finished.', $crawler->html());
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
$fields = ['email' => 'email', 'firstname' => 'firstname', 'lastname' => 'lastname'];
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(Import::QUEUED, $importEntity->getStatus());
|
||||
Assert::assertSame('lead', $importEntity->getObject());
|
||||
Assert::assertSame($fields, $importEntity->getProperties()['fields']);
|
||||
Assert::assertSame(array_values($fields), $importEntity->getProperties()['headers']);
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(1, $importEntity->getInsertedCount());
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount());
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
/** @var LeadRepository $importRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy(['email' => ['john@doe.email', 'ferda@mravenec.email']], ['email' => 'desc']);
|
||||
Assert::assertSame($expectedName, $contacts[0]->getFirstname());
|
||||
Assert::assertCount(2, $contacts);
|
||||
}
|
||||
|
||||
public function testContactPermissionsAreFollowedDuringImport(): void
|
||||
{
|
||||
$filename = 'import-contact-permissions.csv';
|
||||
$permission = [
|
||||
'lead:leads' => ['viewown', 'viewother', 'editown'],
|
||||
'lead:imports' => ['view', 'create', 'edit'],
|
||||
];
|
||||
$role = $this->createRole(false, $permission);
|
||||
$this->createPermission('lead:imports', $role, 1024);
|
||||
$this->createPermission('lead:leads', $role, 14);
|
||||
$user = $this->createUser($role);
|
||||
|
||||
$this->createLead('existing-other@email.tld', 'Existing-other-before');
|
||||
$lead = $this->createLead('existing-owned@email.tld', 'Existing-owned-before');
|
||||
$lead->setOwner($user);
|
||||
$this->em->persist($lead);
|
||||
$this->createCompanyForLead($lead, 'Company One');
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Login newly created non-admin user
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUserIdentifier());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/'.$filename, 'contacts.csv', 'text/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$this->selectCompanyMapping($crawler, $mappingForm);
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created.', $crawler->html());
|
||||
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
\assert($importRepository instanceof ImportRepository);
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => $filename]);
|
||||
|
||||
Assert::assertInstanceOf(Import::class, $importEntity);
|
||||
Assert::assertSame($user->getId(), $importEntity->getCreatedBy());
|
||||
Assert::assertSame($user->getId(), $importEntity->getModifiedBy());
|
||||
Assert::assertSame(Import::QUEUED, $importEntity->getStatus());
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => $filename]);
|
||||
|
||||
Assert::assertInstanceOf(Import::class, $importEntity);
|
||||
Assert::assertSame(3, $importEntity->getLineCount(), '3 rows should be processed as the CSV file contains 3 rows.');
|
||||
Assert::assertSame(0, $importEntity->getInsertedCount(), 'No row should be inserter as the user does not have permission to create contacts.');
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount(), 'There should be one update as the user has the permission to edit his own contacts.');
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
\assert($leadRepository instanceof LeadRepository);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy([], ['id' => 'asc']);
|
||||
Assert::assertCount(2, $contacts, 'There should not be any contact inserted as the user does not have permission to create contacts.');
|
||||
Assert::assertSame('Existing-other-before', $contacts[0]->getFirstname(), 'This contact should not be updated as the user does not have permission to edit others.');
|
||||
Assert::assertSame('Existing-owned-after', $contacts[1]->getFirstname(), 'This contact should be updated as the user has permission to edit own.');
|
||||
|
||||
$eventLogRepository = $this->em->getRepository(LeadEventLog::class);
|
||||
\assert($eventLogRepository instanceof LeadEventLogRepository);
|
||||
|
||||
/** @var LeadEventLog[] $logs */
|
||||
$logs = $eventLogRepository->findBy(['bundle' => 'lead', 'object' => 'import'], ['id' => 'asc']);
|
||||
Assert::assertCount(3, $logs, 'There should be 3 logs connected with the import.');
|
||||
$this->assertInsufficientPermissionError($logs[0], $user);
|
||||
$this->assertInsufficientPermissionError($logs[1], $user);
|
||||
Assert::assertSame('updated', $logs[2]->getAction());
|
||||
Assert::assertArrayNotHasKey('error', $logs[2]->getProperties());
|
||||
}
|
||||
|
||||
public function testImportPublishAndUnpublish(): void
|
||||
{
|
||||
$permission = [
|
||||
'lead:imports' => ['view', 'create', 'edit'],
|
||||
];
|
||||
$role = $this->createRole(false, $permission);
|
||||
$this->createPermission('lead:imports', $role, 36);
|
||||
$user = $this->createUser($role);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// Login newly created non-admin user
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $user->getUsername());
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/contacts.csv', 'contacs.csv', 'itext/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Import process was successfully created. But it will not be processed as you do not have permission to publish.',
|
||||
$crawler->html()
|
||||
);
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertFalse($importEntity->getIsPublished());
|
||||
Assert::assertSame(Import::STOPPED, $importEntity->getStatus());
|
||||
}
|
||||
|
||||
public function testImportFailedWithImportFailedException(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(
|
||||
dirname(__FILE__).'/../Fixtures/contacts.csv',
|
||||
'contacs.csv',
|
||||
'itext/csv'
|
||||
);
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString(
|
||||
'Import process was successfully created. You will be notified when finished.',
|
||||
$crawler->html(),
|
||||
$crawler->html()
|
||||
);
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts.csv']);
|
||||
|
||||
$importEntity->setStatus(4);
|
||||
$importRepository->saveEntity($importEntity);
|
||||
|
||||
$applicationTester = $this->testSymfonyCommand(ImportCommand::COMMAND_NAME, ['--id' => $importEntity->getId()]);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$expectedString = 'Reason: Import could not be triggered since it is not queued nor delayed';
|
||||
|
||||
Assert::assertStringContainsString($expectedString, $applicationTester->getDisplay());
|
||||
}
|
||||
|
||||
public function testImportWithSkipIfExistsFlagTrue(): void
|
||||
{
|
||||
$this->createBooleanField();
|
||||
$lead = $this->createLead('john@doe.email', 'Johny');
|
||||
$this->createCompanyForLead($lead, 'Company A');
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts/import/new');
|
||||
$uploadForm = $crawler->selectButton('Upload')->form();
|
||||
$file = new UploadedFile(dirname(__FILE__).'/../Fixtures/contacts-with-custom-field.csv', 'contacs.csv', 'itext/csv');
|
||||
|
||||
$uploadForm['lead_import[file]']->setValue((string) $file);
|
||||
|
||||
$crawler = $this->client->submit($uploadForm);
|
||||
$mappingForm = $crawler->selectButton('Import')->form();
|
||||
$mappingForm['lead_field_import[skip_if_exists]']->setValue('1');
|
||||
|
||||
// fetch company name mapping value
|
||||
$primaryCompanyOptions = $crawler->filter("#lead_field_import_company > optgroup[label='Primary company']")->filter('option');
|
||||
$optionValues = $primaryCompanyOptions->each(function ($node) {
|
||||
if ('Company Name' === $node->text()) {
|
||||
return $node->attr('value');
|
||||
}
|
||||
});
|
||||
$companyFieldMapping = array_filter($optionValues);
|
||||
$mappingForm['lead_field_import[company]']->setValue(end($companyFieldMapping));
|
||||
$crawler = $this->client->submit($mappingForm);
|
||||
|
||||
Assert::assertStringContainsString('Import process was successfully created. You will be notified when finished.', $crawler->html(), $crawler->html());
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var ImportRepository $importRepository */
|
||||
$importRepository = $this->em->getRepository(Import::class);
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts-with-custom-field.csv']);
|
||||
|
||||
$fields = ['email' => 'email', 'firstname' => 'firstname', 'lastname' => 'lastname', 'company' => 'companyname', 'custom_boolean_field' => 'custom_boolean_field'];
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame('lead', $importEntity->getObject());
|
||||
Assert::assertEquals($fields, $importEntity->getProperties()['fields']);
|
||||
Assert::assertEquals(array_keys($fields), $importEntity->getProperties()['headers']);
|
||||
|
||||
$this->testSymfonyCommand(ImportCommand::COMMAND_NAME);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
/** @var Import $importEntity */
|
||||
$importEntity = $importRepository->findOneBy(['originalFile' => 'contacts-with-custom-field.csv']);
|
||||
|
||||
Assert::assertNotNull($importEntity);
|
||||
Assert::assertSame(2, $importEntity->getLineCount());
|
||||
Assert::assertSame(1, $importEntity->getInsertedCount());
|
||||
Assert::assertSame(1, $importEntity->getUpdatedCount());
|
||||
Assert::assertSame(Import::IMPORTED, $importEntity->getStatus());
|
||||
|
||||
/** @var LeadRepository $importRepository */
|
||||
$leadRepository = $this->em->getRepository(Lead::class);
|
||||
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $leadRepository->findBy(['email' => ['john@doe.email', 'ferda@mravenec.email']], ['email' => 'desc']);
|
||||
Assert::assertSame('Johny', $contacts[0]->getFirstname());
|
||||
Assert::assertSame('Company A', $contacts[0]->getCompany());
|
||||
Assert::assertSame('Company B', $contacts[1]->getCompany());
|
||||
Assert::assertCount(2, $contacts);
|
||||
}
|
||||
|
||||
private function setPhoneFieldIsRequired(bool $required): void
|
||||
{
|
||||
/** @var LeadFieldRepository $fieldRepository */
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
/** @var LeadField $phoneField */
|
||||
$phoneField = $fieldRepository->findOneBy(['alias' => 'phone']);
|
||||
|
||||
$phoneField->setIsRequired($required);
|
||||
$fieldRepository->saveEntity($phoneField);
|
||||
}
|
||||
|
||||
private function createLead(?string $email = null, string $firstName = ''): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
if (!empty($email)) {
|
||||
$lead->setEmail($email);
|
||||
}
|
||||
$lead->setFirstname($firstName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $permission
|
||||
*/
|
||||
private function createRole(bool $isAdmin = false, array $permission = []): Role
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName('Role');
|
||||
$role->setIsAdmin($isAdmin);
|
||||
$role->setRawPermissions($permission);
|
||||
$this->em->persist($role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
private function createPermission(string $rawPermission, Role $role, int $bitwise): void
|
||||
{
|
||||
$parts = explode(':', $rawPermission);
|
||||
$permission = new Permission();
|
||||
$permission->setBundle($parts[0]);
|
||||
$permission->setName($parts[1]);
|
||||
$permission->setRole($role);
|
||||
$permission->setBitwise($bitwise);
|
||||
$this->em->persist($permission);
|
||||
}
|
||||
|
||||
private function createUser(Role $role): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('John');
|
||||
$user->setLastName('Doe');
|
||||
$user->setUsername('john.doe');
|
||||
$user->setEmail('john.doe@email.com');
|
||||
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
$user->setPassword($hasher->hash('Maut1cR0cks!'));
|
||||
$user->setRole($role);
|
||||
$this->em->persist($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createBooleanField(): void
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)
|
||||
->findOneBy(['username' => $this->clientServer['PHP_AUTH_USER'] ?? 'admin']);
|
||||
$this->loginUser($user);
|
||||
$field = new LeadField();
|
||||
$field->setType('boolean');
|
||||
$field->setObject('lead');
|
||||
$field->setGroup('core');
|
||||
$field->setLabel('Test boolean field');
|
||||
$field->setAlias('custom_boolean_field');
|
||||
$field->setDefaultValue('0');
|
||||
$field->setProperties(['no' => 'No', 'yes' => 'Yes']);
|
||||
|
||||
/** @var FieldModel $fieldModel */
|
||||
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
|
||||
$fieldModel->saveEntity($field);
|
||||
}
|
||||
|
||||
private function createCompanyForLead(Lead $lead, string $companyName): void
|
||||
{
|
||||
$company = new Company();
|
||||
$company->setName($companyName);
|
||||
$this->em->persist($company);
|
||||
|
||||
// add company to lead
|
||||
$lead->setCompany($companyName);
|
||||
$this->em->persist($lead);
|
||||
|
||||
// set primary company for lead
|
||||
$companyLead = new CompanyLead();
|
||||
$companyLead->setCompany($company);
|
||||
$companyLead->setLead($lead);
|
||||
$companyLead->setDateAdded(new \DateTime());
|
||||
$companyLead->setPrimary(true);
|
||||
$this->em->persist($companyLead);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function validateDataProvider(): iterable
|
||||
{
|
||||
yield ['0', 'John'];
|
||||
yield ['1', 'Johny'];
|
||||
}
|
||||
|
||||
private function assertInsufficientPermissionError(LeadEventLog $log, User $user): void
|
||||
{
|
||||
Assert::assertSame('failed', $log->getAction(), 'The insertion should fail as the user does not have permission to create contacts.');
|
||||
Assert::assertSame(sprintf('User \'%s\' has insufficient permissions', $user->getUserIdentifier()), $log->getProperties()['error'], 'There should be an insufficient permission error.');
|
||||
}
|
||||
|
||||
private function selectCompanyMapping(Crawler $crawler, Form $mappingForm): void
|
||||
{
|
||||
$options = $crawler->filter("#lead_field_import_company > optgroup[label='Primary company']")->filter('option');
|
||||
$values = array_filter($options->each(function ($node) {
|
||||
if ('Company Name' === $node->text()) {
|
||||
return $node->attr('value');
|
||||
}
|
||||
}));
|
||||
$mappingForm['lead_field_import[company]']->setValue(end($values));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
|
||||
class LeadCompanyControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_allow_multiple_companies'] = 0;
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testSimpleCompanyFeature(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', 's/contacts/new/');
|
||||
$multiple = $crawler->filterXPath('//*[@id="lead_companies"]')->attr('multiple');
|
||||
self::assertNull($multiple);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class LeadControllerListingPageTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['contact_columns'] = ['name', 'location', 'email'];
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $location
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataForContactListing')]
|
||||
public function testContactListingForLocation(array $location, string $expected): void
|
||||
{
|
||||
$this->createContact($location);
|
||||
|
||||
$crawler = $this->client->request('GET', 's/contacts');
|
||||
$rowContent = $crawler->filterXPath("//table[@id='leadTable']//tbody//tr");
|
||||
|
||||
Assert::assertStringEndsWith($expected, $rowContent->text());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array<int, string|string[]>>
|
||||
*/
|
||||
public static function dataForContactListing(): iterable
|
||||
{
|
||||
yield 'With no location' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => '',
|
||||
'setState' => '',
|
||||
'setCountry' => '',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe john@doe.example.com',
|
||||
];
|
||||
|
||||
yield 'With whole location details' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => 'Pune',
|
||||
'setState' => 'MH',
|
||||
'setCountry' => 'India',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe Pune, MH john@doe.example.com',
|
||||
];
|
||||
|
||||
yield 'With only City for location' => [
|
||||
// Location Details
|
||||
[
|
||||
'setCity' => 'Pune',
|
||||
'setState' => '',
|
||||
'setCountry' => '',
|
||||
],
|
||||
// Expected suffice
|
||||
'John Doe Pune john@doe.example.com',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $location
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createContact(array $location = []): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('John');
|
||||
$contact->setLastname('Doe');
|
||||
$contact->setEmail('john@doe.example.com');
|
||||
|
||||
foreach ($location as $name => $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
$contact->$name($value);
|
||||
}
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
class LeadDetailFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testCustomFieldOrderIsRespected(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname('John');
|
||||
$lead->setLastname('Doe');
|
||||
$lead->setEmail('john@his-site.com');
|
||||
$this->em->persist($lead);
|
||||
|
||||
$fieldRepository = $this->em->getRepository(LeadField::class);
|
||||
|
||||
/** @var LeadField[] $fields */
|
||||
$fields = $fieldRepository->findBy(['object' => 'lead', 'group' => 'core'], [
|
||||
'label' => 'desc',
|
||||
'id' => 'desc',
|
||||
]);
|
||||
$order = 0;
|
||||
|
||||
// re-order fields by the label
|
||||
foreach ($fields as $field) {
|
||||
$field->setOrder(++$order);
|
||||
$this->em->persist($field);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
// initialize lead fields to adjust the expected core labels
|
||||
$lead->setFields([
|
||||
'core' => [
|
||||
'First Name' => [
|
||||
'value' => 'John',
|
||||
],
|
||||
'Last Name' => [
|
||||
'value' => 'Doe',
|
||||
],
|
||||
'Email' => [
|
||||
'value' => 'john@his-site.com',
|
||||
],
|
||||
'Primary company' => [
|
||||
'value' => null,
|
||||
],
|
||||
'Points' => [
|
||||
'value' => 0,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$leadFields = array_filter($lead->getFields(true), fn ($value) => isset($value['value']));
|
||||
$leadFields = array_keys($leadFields);
|
||||
|
||||
// get expected core labels
|
||||
$expectedLabels = $this->connection->createQueryBuilder()
|
||||
->select('label')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_fields')
|
||||
->where('object = "lead"')
|
||||
->andWhere('field_group = "core"')
|
||||
->andWhere('label IN (:leadFields)')
|
||||
->orderBy('field_order')
|
||||
->setParameter(
|
||||
'leadFields',
|
||||
$leadFields,
|
||||
ArrayParameterType::STRING
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchFirstColumn();
|
||||
|
||||
$expectedLabels = array_merge(['Created on', 'ID'], $expectedLabels);
|
||||
|
||||
$crawler = $this->client->request('GET', sprintf('/s/contacts/view/%d', $lead->getId()));
|
||||
|
||||
// get actual core labels
|
||||
$actualLabels = $crawler->filter('#lead-details table')
|
||||
->first()
|
||||
->filter('td:first-child')
|
||||
->extract(['_text']);
|
||||
$actualLabels = array_map('trim', $actualLabels);
|
||||
|
||||
Assert::assertSame($expectedLabels, $actualLabels);
|
||||
}
|
||||
|
||||
public function testLeadViewPreventsXSS(): void
|
||||
{
|
||||
$firstName = 'aaa" onmouseover=alert(1) a="';
|
||||
$lead = new Lead();
|
||||
$lead->setFirstname($firstName);
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', sprintf('/s/contacts/view/%d', $lead->getId()));
|
||||
|
||||
$anchorTag = $crawler->filter('#toolbar ul.dropdown-menu-right li')->first()->filter('a');
|
||||
$mouseOver = $anchorTag->attr('onmouseover');
|
||||
$dataHeader = $anchorTag->attr('data-header');
|
||||
|
||||
Assert::assertNull($mouseOver);
|
||||
Assert::assertSame(sprintf('Campaigns for %s', $firstName), $dataHeader);
|
||||
$response = $this->client->getResponse();
|
||||
// Make sure the data-target-url is not an absolute URL
|
||||
Assert::assertStringContainsString(sprintf('data-target-url="/s/contacts/view/%s/stats"', $lead->getId()), $response->getContent());
|
||||
}
|
||||
|
||||
public function testLeadDetailPageForSocialTabInDetailsCollapsibleForNoData(): void
|
||||
{
|
||||
$contact = new Lead();
|
||||
$contact->setEmail('john@doe.corp');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', 's/contacts/view/'.$contact->getId());
|
||||
$data = $crawler->filterXPath('//div[@id="social"]//td');
|
||||
$this->assertCount(1, $data);
|
||||
|
||||
$translator = static::getContainer()->get('translator');
|
||||
$this->assertStringContainsString($translator->trans('mautic.lead.field.group.no_data'), $data->text());
|
||||
}
|
||||
|
||||
public function testLeadDetailPageForSocialTabInDetailsCollapsible(): void
|
||||
{
|
||||
$crawler = $this->client->request('GET', 's/contacts/new/');
|
||||
|
||||
$fbLink = 'https://fb.com/john_doe_test';
|
||||
$form = $crawler->selectButton('Save & Close')->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'lead[firstname]' => 'John',
|
||||
'lead[lastname]' => 'Doe',
|
||||
'lead[email]' => 'john@doe.corp',
|
||||
'lead[facebook]' => $fbLink,
|
||||
]
|
||||
);
|
||||
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$fbProfile = $crawler->filterXPath('//div[@id="social"]//td[2]');
|
||||
|
||||
$this->assertCount(1, $fbProfile);
|
||||
$this->assertStringContainsString($fbLink, $fbProfile->text());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
|
||||
final class LeadListProjectSearchFunctionalTest 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');
|
||||
|
||||
$segmentAlpha = $this->createSegment('Segment Alpha');
|
||||
$segmentBeta = $this->createSegment('Segment Beta');
|
||||
$this->createSegment('Segment Gamma');
|
||||
$this->createSegment('Segment Delta');
|
||||
|
||||
$segmentAlpha->addProject($projectOne);
|
||||
$segmentAlpha->addProject($projectTwo);
|
||||
$segmentBeta->addProject($projectTwo);
|
||||
$segmentBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/segments', '/s/segments']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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' => ['Segment Alpha', 'Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND segment name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR segment name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Beta', 'Segment Gamma'],
|
||||
'unexpectedEntities' => ['Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Beta'],
|
||||
'unexpectedEntities' => ['Segment Gamma', 'Segment Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Segment Alpha', 'Segment Gamma', 'Segment Delta'],
|
||||
'unexpectedEntities' => ['Segment Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createSegment(string $name): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(str_replace(' ', '-', mb_strtolower($name)));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\DataCollector\DoctrineDataCollector;
|
||||
use Doctrine\Bundle\DoctrineBundle\Twig\DoctrineExtension;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class LeadListSearchFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected array $clientOptions = ['debug' => true];
|
||||
|
||||
/** @noinspection SqlResolve */
|
||||
public function testSegmentSearch(): void
|
||||
{
|
||||
// create some leads
|
||||
$leadOne = $this->createLead('one');
|
||||
$leadTwo = $this->createLead('two');
|
||||
$leadThree = $this->createLead('three');
|
||||
$leadFour = $this->createLead('four');
|
||||
$leadFive = $this->createLead('five');
|
||||
$leadSix = $this->createLead('six');
|
||||
|
||||
// add some leads in lists
|
||||
$listOne = $this->createLeadList('first-list', $leadOne, $leadTwo, $leadThree);
|
||||
$listTwo = $this->createLeadList('second-list', $leadOne, $leadFour, $leadFive, $leadSix);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->enableProfiler();
|
||||
$prefix = static::getContainer()->getParameter('mautic.db_table_prefix');
|
||||
$previousQueries = [];
|
||||
|
||||
// non-existent segment search
|
||||
$this->assertSearchResult('segment%3AnonExistent', [], [$leadOne, $leadTwo, $leadThree, $leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = 'nonexistent'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN (0)))) AND (l.date_identified IS NOT NULL)",
|
||||
], $previousQueries);
|
||||
|
||||
// first-list segment search
|
||||
$this->assertSearchResult('segment%3A'.$listOne->getAlias(), [$leadOne, $leadTwo, $leadThree], [$leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listOne->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
$this->assertSearchResult('!segment%3A'.$listOne->getAlias(), [$leadFour, $leadFive, $leadSix], [$leadOne, $leadTwo, $leadThree]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listOne->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listOne->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
|
||||
// second-list segment search
|
||||
$this->assertSearchResult('segment%3A'.$listTwo->getAlias(), [$leadOne, $leadFour, $leadFive, $leadSix], [$leadTwo, $leadThree]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listTwo->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
$this->assertSearchResult('!segment%3A'.$listTwo->getAlias(), [$leadTwo, $leadThree], [$leadOne, $leadFour, $leadFive, $leadSix]);
|
||||
$this->assertQueries([
|
||||
"SELECT list.id FROM {$prefix}lead_lists list WHERE list.alias = '{$listTwo->getAlias()}'",
|
||||
"SELECT COUNT(l.id) as count FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL)",
|
||||
"SELECT l.* FROM {$prefix}leads l USE INDEX FOR JOIN ({$prefix}lead_date_added) WHERE (NOT EXISTS(SELECT 1 FROM {$prefix}lead_lists_leads lla WHERE (l.id = lla.lead_id) AND (lla.manually_removed = 0) AND (lla.leadlist_id IN ('{$listTwo->getId()}')))) AND (l.date_identified IS NOT NULL) ORDER BY l.last_active DESC, l.id DESC LIMIT 30",
|
||||
], $previousQueries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $expectedLeads
|
||||
* @param Lead[] $notExpectedLeads
|
||||
*/
|
||||
private function assertSearchResult(string $search, array $expectedLeads, array $notExpectedLeads): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/contacts?search='.$search);
|
||||
self::assertResponseIsSuccessful();
|
||||
$responseText = $crawler->text();
|
||||
|
||||
foreach ($expectedLeads as $expectedLead) {
|
||||
Assert::assertStringContainsString($expectedLead->getEmail(), $responseText, sprintf('Lead with the email "%s" should be in the result.', $expectedLead->getEmail()));
|
||||
}
|
||||
|
||||
foreach ($notExpectedLeads as $notExpectedLead) {
|
||||
Assert::assertStringNotContainsString($notExpectedLead->getEmail(), $responseText, sprintf('Lead with the email "%s" should not be in the result.', $notExpectedLead->getEmail()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $expectedQueries
|
||||
* @param string[] $previousQueries
|
||||
*/
|
||||
private function assertQueries(array $expectedQueries, array &$previousQueries): void
|
||||
{
|
||||
/** @var DoctrineDataCollector $dbCollector */
|
||||
$dbCollector = $this->client->getProfile()->getCollector('db');
|
||||
$allQueries = $dbCollector->getQueries()['default'];
|
||||
$queries = array_diff_key($allQueries, $previousQueries);
|
||||
$previousQueries = $allQueries;
|
||||
$doctrineExtension = new DoctrineExtension();
|
||||
|
||||
$queries = array_map(function (array $query) use ($doctrineExtension) {
|
||||
return $doctrineExtension->replaceQueryParameters($query['sql'], $query['params']);
|
||||
}, $queries);
|
||||
|
||||
foreach ($expectedQueries as $expectedQuery) {
|
||||
$matchedQueries = array_filter($queries, function (string $query) use ($expectedQuery) {
|
||||
return $expectedQuery === $query;
|
||||
});
|
||||
Assert::assertCount(1, $matchedQueries, sprintf('The query "%s" was expected to be executed once.', $expectedQuery));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLead(string $lastName): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setLastname($lastName);
|
||||
$lead->setEmail(sprintf('%s@mail.tld', $lastName));
|
||||
$this->em->persist($lead);
|
||||
|
||||
return $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead ...$leads
|
||||
*
|
||||
* @throws ORMException
|
||||
*/
|
||||
private function createLeadList(string $name, ...$leads): LeadList
|
||||
{
|
||||
$leadList = new LeadList();
|
||||
$leadList->setName($name);
|
||||
$leadList->setPublicName($name);
|
||||
$leadList->setAlias(mb_strtolower($name));
|
||||
$this->em->persist($leadList);
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$this->addLeadToList($lead, $leadList);
|
||||
}
|
||||
|
||||
return $leadList;
|
||||
}
|
||||
|
||||
private function addLeadToList(Lead $leadOne, LeadList $sourceList): void
|
||||
{
|
||||
$listLead = new ListLead();
|
||||
$listLead->setLead($leadOne);
|
||||
$listLead->setList($sourceList);
|
||||
$listLead->setDateAdded(new \DateTime());
|
||||
$this->em->persist($listLead);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,817 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Command\SegmentCountCacheCommand;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\LeadListRepository;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\LeadBundle\Model\ListModel;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\ProjectBundle\Model\ProjectModel;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class ListControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private ListModel $listModel;
|
||||
|
||||
private LeadListRepository $listRepo;
|
||||
|
||||
protected SegmentCountCacheHelper $segmentCountCacheHelper;
|
||||
|
||||
private LeadRepository $leadRepo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['update_segment_contact_count_in_background'] = 'testSegmentCountInBackground' === $this->name();
|
||||
parent::setUp();
|
||||
$this->listModel = static::getContainer()->get('mautic.lead.model.list');
|
||||
\assert($this->listModel instanceof ListModel);
|
||||
$this->listRepo = $this->listModel->getRepository();
|
||||
\assert($this->listRepo instanceof LeadListRepository);
|
||||
$leadModel = static::getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($leadModel instanceof LeadModel);
|
||||
$this->segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache');
|
||||
$this->leadRepo = $leadModel->getRepository();
|
||||
}
|
||||
|
||||
public function testUnpublishUsedSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$list1->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
]];
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
$expectedErrorMessage = sprintf('This segment is used in %s, please go back and check segments before unpublishing', $list2->getName());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'lead.list', 'id' => $list1->getId()]);
|
||||
$this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString($expectedErrorMessage, $this->client->getResponse()->getContent());
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$list1->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertStringContainsString($expectedErrorMessage, $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
public function testUnpublishUnUsedSegment(): void
|
||||
{
|
||||
$filter = [[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
]];
|
||||
$list1 = $this->saveSegment('s1', 's1', $filter);
|
||||
$list2 = $this->saveSegment('s2', 's2', $filter);
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/ajax', ['action' => 'togglePublishStatus', 'model' => 'lead.list', 'id' => $list1->getId()]);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$list2->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$rows = $this->listRepo->findAll();
|
||||
$this->assertCount(2, $rows);
|
||||
$this->assertFalse($rows[0]->isPublished());
|
||||
$this->assertFalse($rows[1]->isPublished());
|
||||
}
|
||||
|
||||
public function testBCSegmentWithPageHitInLeadObject(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Legacy Url Hit segment',
|
||||
's1',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'hit_url',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'filter' => 'unicorn',
|
||||
'display' => null,
|
||||
'operator' => '=',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
Assert::assertGreaterThan(0, $crawler->filter('#leadlist_filters_0_operator option')->count());
|
||||
}
|
||||
|
||||
public function testSegmentWithProject(): void
|
||||
{
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
|
||||
$segment = $this->saveSegment('Segment with Project', 'st1', $filters);
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
|
||||
$projectModel = self::getContainer()->get(ProjectModel::class);
|
||||
\assert($projectModel instanceof ProjectModel);
|
||||
$projectModel->saveEntity($project);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedSegment = $this->listRepo->find($segment->getId());
|
||||
Assert::assertSame($project->getId(), $savedSegment->getProjects()->first()->getId());
|
||||
}
|
||||
|
||||
private function saveSegment(string $name, string $alias, array $filters = [], ?LeadList $segment = null): LeadList
|
||||
{
|
||||
$segment ??= new LeadList();
|
||||
$segment->setName($name)->setAlias($alias)->setFilters($filters);
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCount(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
$segment = $this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Save manual segment without filters.
|
||||
$manualSegment = $this->saveSegment('Lead List 2', 'lead-list-2');
|
||||
$manualSegmentId = $manualSegment->getId();
|
||||
|
||||
// Verify last built date is not set.
|
||||
self::assertNull($segment->getLastBuiltDate());
|
||||
|
||||
// Check segment count UI for no contacts for manual segment.
|
||||
// And check the filtered segment is Building
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('Building', $html);
|
||||
self::assertSame('label label-info col-count', $spClass);
|
||||
$html = $this->getSegmentCountHtml($crawler, $manualSegmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $manualSegmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Add 4 contacts.
|
||||
$contacts = $this->saveContacts();
|
||||
$contact1Id = $contacts[0]->getId();
|
||||
|
||||
// Rebuild segment - set current count to the cache.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
// Verify last built date is set.
|
||||
$this->em->detach($segment);
|
||||
$segment = $this->listRepo->find($segmentId);
|
||||
self::assertNotNull($segment->getLastBuiltDate());
|
||||
|
||||
// Set last built date in the future to allow testing without waiting.
|
||||
// (Same second built date as the modified date is shown as "Building" still in the UI).
|
||||
$segment->setLastBuiltDate(new \DateTime('+5 seconds'));
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count UI for 3 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 3 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count AJAX for 3 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 3 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(3, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Save filtered segment again to trigger rebuild label, setting last built date in the past.
|
||||
$this->em->detach($segment);
|
||||
$segment = $this->listRepo->find($segmentId);
|
||||
$segment->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
// Date modified only updates on specific changes, so change name.
|
||||
$segment->setName('Lead List 1 Updated');
|
||||
$this->listModel->saveEntity($segment);
|
||||
|
||||
// Check segment count UI for bulding with 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('Building (4 Contacts)', $html);
|
||||
self::assertSame('label label-info col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX for building 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('Building (4 Contacts)', $response['content']['html']);
|
||||
self::assertSame('label label-info col-count', $response['content']['className']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testSegmentCountInBackground(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
|
||||
$segment = $this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$segmentId = $segment->getId();
|
||||
$this->segmentCountCacheHelper->deleteSegmentContactCount($segmentId);
|
||||
|
||||
// Check segment count UI for no contacts.
|
||||
usleep(1000000);
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
|
||||
// Add 4 contacts.
|
||||
$contacts = $this->saveContacts();
|
||||
$contact1Id = $contacts[0]->getId();
|
||||
|
||||
// Rebuild segment - set current count to the cache.
|
||||
$this->testSymfonyCommand('mautic:segments:update', ['-i' => $segmentId, '--env' => 'test']);
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 3 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 3 Contacts', $html);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count UI for 4 contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
self::assertSame('View 4 Contacts', $html);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Remove 1 contact from segment.
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contact/'.$contact1Id.'/remove');
|
||||
self::assertSame('{"success":1}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count AJAX for 3 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 3 Contacts', $response['content']['html']);
|
||||
self::assertSame(3, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
|
||||
// Add 1 contact back to segment.
|
||||
$parameters = ['ids' => [$contact1Id]];
|
||||
$this->client->request(Request::METHOD_POST, '/api/segments/'.$segmentId.'/contacts/add', $parameters);
|
||||
self::assertSame('{"success":1,"details":{"'.$contact1Id.'":{"success":true}}}', $this->client->getResponse()->getContent());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->testSymfonyCommand(SegmentCountCacheCommand::COMMAND_NAME);
|
||||
|
||||
// Check segment count AJAX for 4 contacts.
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('View 4 Contacts', $response['content']['html']);
|
||||
self::assertSame(4, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
public function testSegmentClone(): void
|
||||
{
|
||||
$segment = $this->saveSegment('Test Segment', 'testsegment');
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Number of segments before clone
|
||||
$segmentsCountBefore = $this->em->getRepository(LeadList::class)->count([]);
|
||||
// Go to clone segment action
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/'.(string) $segmentId);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
// First submit
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
// Second submit
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
// Number of segments after clone
|
||||
$segmentsCountAfter = $this->em->getRepository(LeadList::class)->count([]);
|
||||
// Check that just one segment was created
|
||||
$this->assertSame($segmentsCountBefore + 1, $segmentsCountAfter);
|
||||
}
|
||||
|
||||
public function testSegmentAliasCreation(): void
|
||||
{
|
||||
$segment = $this->saveSegment('Test Segment Alias', 'test-segment-alias');
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Clone segment
|
||||
$aliasFirst = $this->getAliasWhenCloneSegment($segmentId);
|
||||
// Clone segment again
|
||||
$aliasSecond = $this->getAliasWhenCloneSegment($segmentId);
|
||||
// Check that aliases are not the same
|
||||
$this->assertNotSame($aliasFirst, $aliasSecond);
|
||||
}
|
||||
|
||||
private function getAliasWhenCloneSegment(int $segmentId): string
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/'.(string) $segmentId);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
// Save cloned segment
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), 'Correct Apply');
|
||||
|
||||
return $crawler->filter('#leadlist_alias')->attr('value');
|
||||
}
|
||||
|
||||
public function testSegmentNotFoundOnAjax(): void
|
||||
{
|
||||
// Emulate invalid request parameter.
|
||||
$parameter = ['id' => 'ABC'];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
|
||||
self::assertSame('No Contacts', $response['content']['html']);
|
||||
self::assertSame(0, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_NOT_FOUND, $response['statusCode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function saveContacts(int $count = 4): array
|
||||
{
|
||||
$contacts = [];
|
||||
|
||||
for ($i = 1; $i <= $count; ++$i) {
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('Contact '.$i)->setEmail('contact'.$i.'@example.com');
|
||||
$contacts[] = $contact;
|
||||
}
|
||||
|
||||
$this->leadRepo->saveEntities($contacts);
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function getSegmentCountHtml(Crawler $crawler, int $id): string
|
||||
{
|
||||
$content = $crawler->filter('span.col-count[data-id="'.$id.'"] a')->html();
|
||||
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function getSegmentCountClass(Crawler $crawler, int $id): string
|
||||
{
|
||||
$class = $crawler->filter('span.col-count[data-id="'.$id.'"]')->attr('class');
|
||||
|
||||
return trim($class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameter
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function callGetLeadCountAjaxRequest(array $parameter): array
|
||||
{
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=lead:getLeadCount', $parameter);
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
return [
|
||||
'content' => json_decode($clientResponse->getContent(), true),
|
||||
'statusCode' => $this->client->getResponse()->getStatusCode(),
|
||||
];
|
||||
}
|
||||
|
||||
public function testCloneSegment(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Clone Segment',
|
||||
'clonesegment',
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/clone/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[alias]']->setValue('clonesegment2');
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$rows = $this->listRepo->findAll();
|
||||
$this->assertCount(2, $rows);
|
||||
|
||||
$this->assertSame('clonesegment', $rows[0]->getAlias());
|
||||
$this->assertSame('clonesegment2', $rows[1]->getAlias());
|
||||
}
|
||||
|
||||
public function testSegmentFilterIcon(): void
|
||||
{
|
||||
// Save segment.
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => '!empty',
|
||||
],
|
||||
];
|
||||
$this->saveSegment('Lead List 1', 'lead-list-1', $filters);
|
||||
$this->saveSegment('Lead List 2', 'lead-list-2');
|
||||
|
||||
// Check segment count UI for no contacts.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$leadListsTableRows = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr");
|
||||
$this->assertEquals(2, $leadListsTableRows->count());
|
||||
$secondColumnOfLine = $leadListsTableRows->first()->filterXPath('//td[2]//div//i[@class="ri-fw ri-filter-2-fill fs-14"]')->count();
|
||||
$this->assertEquals(1, $secondColumnOfLine);
|
||||
$secondColumnOfLine = $leadListsTableRows->eq(1)->filterXPath('//td[2]//div//i[@class="ri-fw ri-filter-2-fill fs-14"]')->count();
|
||||
$this->assertEquals(0, $secondColumnOfLine);
|
||||
}
|
||||
|
||||
public function testUnpublishedSegmentDoesNotShowRebuildingLabel(): void
|
||||
{
|
||||
// Create a segment that would normally show "Building" label
|
||||
$segment = $this->saveSegment('Unpublished Segment', 'unpublished-segment', [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'email',
|
||||
'object' => 'lead',
|
||||
'type' => 'email',
|
||||
'operator' => '!empty',
|
||||
'display' => '',
|
||||
],
|
||||
]);
|
||||
|
||||
// Set last built date in the past to trigger "Building" label for published segments
|
||||
$segment->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
|
||||
// Unpublish the segment - this should prevent "Building" label
|
||||
$segment->setIsPublished(false);
|
||||
$this->listModel->saveEntity($segment);
|
||||
$this->em->clear();
|
||||
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
// Check segment count UI - should show "No Contacts" rather than "Building"
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$html = $this->getSegmentCountHtml($crawler, $segmentId);
|
||||
$spClass = $this->getSegmentCountClass($crawler, $segmentId);
|
||||
self::assertSame('No Contacts', $html);
|
||||
self::assertSame('label label-gray col-count', $spClass);
|
||||
|
||||
// Check segment count AJAX - should also show "No Contacts"
|
||||
$parameter = ['id' => $segmentId];
|
||||
$response = $this->callGetLeadCountAjaxRequest($parameter);
|
||||
self::assertSame('No Contacts', $response['content']['html']);
|
||||
self::assertSame('label label-gray col-count', $response['content']['className']);
|
||||
self::assertSame(0, $response['content']['leadCount']);
|
||||
self::assertSame(Response::HTTP_OK, $response['statusCode']);
|
||||
}
|
||||
|
||||
public function testSegmentWarningIcon(): void
|
||||
{
|
||||
$segmentWithOldLastRebuildDate = $this->saveSegment('TEST-Warning-Segment', 'test-warning-segment');
|
||||
$segmentWithFreshLastRebuildDate = $this->saveSegment('TEST-Fresh-Segment', 'test-fresh-segment');
|
||||
$segmentUnpublished = $this->saveSegment('TEST-Unpublished-Segment', 'test-unpublished-segment');
|
||||
|
||||
$segmentWithOldLastRebuildDate->setLastBuiltDate(new \DateTime('-1 year'));
|
||||
$segmentWithFreshLastRebuildDate->setLastBuiltDate(new \DateTime('now'));
|
||||
$segmentUnpublished->setIsPublished(false);
|
||||
|
||||
$this->em->persist($segmentWithOldLastRebuildDate);
|
||||
$this->em->persist($segmentWithFreshLastRebuildDate);
|
||||
$this->em->persist($segmentUnpublished);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
|
||||
$warningSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Warning-Segment')]");
|
||||
$warningIcon = $warningSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(1, $warningIcon->count());
|
||||
|
||||
$freshSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Fresh-Segment')]");
|
||||
$warningIcon = $freshSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(0, $warningIcon->count());
|
||||
|
||||
$unpublishedSegmentRow = $crawler->filterXPath("//table[@id='leadListTable']//tbody//tr[contains(., 'TEST-Unpublished-Segment')]");
|
||||
$warningIcon = $unpublishedSegmentRow->filterXPath('.//i[@class="text-danger ri-error-warning-line fs-14"]');
|
||||
$this->assertEquals(0, $warningIcon->count());
|
||||
}
|
||||
|
||||
public function testBatchDeleteWithEmptyMembership(): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Empty Members',
|
||||
'empty-members',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'object' => 'lead',
|
||||
'type' => 'leadlist',
|
||||
'filter' => null,
|
||||
'display' => null,
|
||||
'operator' => 'empty',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$segmentId = $segment->getId();
|
||||
|
||||
$this->setCsrfHeader();
|
||||
$this->client->xmlHttpRequest('POST', "s/segments/batchDelete?ids=[\"{$segmentId}\"]");
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $clientResponse->getStatusCode(), $clientResponse->getContent());
|
||||
$this->assertStringContainsString('1 segments have been deleted!', $clientResponse->getContent());
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$segmentExistCheck = $this->listRepo->find($segmentId);
|
||||
Assert::assertNull($segmentExistCheck);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dateFieldProvider')]
|
||||
public function testWarningOnInvalidDateField(?string $filter, bool $shouldContainError, string $operator = '='): void
|
||||
{
|
||||
$segment = $this->saveSegment(
|
||||
'Date Segment',
|
||||
'ds',
|
||||
[
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'date_added',
|
||||
'object' => 'lead',
|
||||
'type' => 'date',
|
||||
'filter' => $filter,
|
||||
'display' => null,
|
||||
'operator' => $operator,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$this->client->submit($form);
|
||||
$this->assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
if ($shouldContainError) {
|
||||
$this->assertStringContainsString('Date field filter value "'.$filter.'" is invalid', $this->client->getResponse()->getContent());
|
||||
} else {
|
||||
$this->assertStringNotContainsString('Date field filter value', $this->client->getResponse()->getContent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, bool|string|null>>
|
||||
*/
|
||||
public static function dateFieldProvider(): array
|
||||
{
|
||||
return [
|
||||
['Today', true],
|
||||
['birthday', false],
|
||||
['2023-01-01 11:00', false],
|
||||
['2023-01-01 11:00:00', false],
|
||||
['2023-01-01', false],
|
||||
['next week', false],
|
||||
[null, false],
|
||||
['\b\d{4}-(10|11|12)-\d{2}\b', false, 'regexp'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRecentActivityFeedOnSegmentDetailsPage(): void
|
||||
{
|
||||
// Create segment
|
||||
$segment = $this->saveSegment('Date Segment', 'ds');
|
||||
$this->em->clear();
|
||||
|
||||
// Update segment
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$this->client->submit($form);
|
||||
|
||||
// View segment
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$translator = self::getContainer()->get('translator');
|
||||
|
||||
$this->assertStringContainsString($translator->trans('mautic.core.recent.activity'), $this->client->getResponse()->getContent());
|
||||
$this->assertCount(2, $crawler->filterXPath('//ul[contains(@class, "media-list-feed")]/li'));
|
||||
}
|
||||
|
||||
public function testActiveContactsStatExcludesDnc(): void
|
||||
{
|
||||
$segment = $this->saveSegment('active-test', 'active-test');
|
||||
$contact1 = new Lead();
|
||||
$contact1->setFirstname('Active');
|
||||
$this->em->persist($contact1);
|
||||
$contact2 = new Lead();
|
||||
$contact2->setFirstname('DNC');
|
||||
$this->em->persist($contact2);
|
||||
$this->em->flush();
|
||||
$segmentContact1 = new \Mautic\LeadBundle\Entity\ListLead();
|
||||
$segmentContact1->setList($segment);
|
||||
$segmentContact1->setLead($contact1);
|
||||
$segmentContact1->setDateAdded(new \DateTime());
|
||||
$segmentContact1->setManuallyAdded(false);
|
||||
$segmentContact1->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContact1);
|
||||
$segmentContact2 = new \Mautic\LeadBundle\Entity\ListLead();
|
||||
$segmentContact2->setList($segment);
|
||||
$segmentContact2->setLead($contact2);
|
||||
$segmentContact2->setDateAdded(new \DateTime());
|
||||
$segmentContact2->setManuallyAdded(false);
|
||||
$segmentContact2->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContact2);
|
||||
$this->em->flush();
|
||||
$dnc = new \Mautic\LeadBundle\Entity\DoNotContact();
|
||||
$dnc->setChannel('email');
|
||||
$dnc->setLead($contact2);
|
||||
$dnc->setReason(\Mautic\LeadBundle\Entity\DoNotContact::UNSUBSCRIBED);
|
||||
$dnc->setDateAdded(new \DateTime());
|
||||
$this->em->persist($dnc);
|
||||
$this->em->flush();
|
||||
$this->client->request('GET', sprintf('/s/segments/view/%d', $segment->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful();
|
||||
$html = $response->getContent();
|
||||
$this->assertStringContainsString('Total contacts', $html);
|
||||
$this->assertStringContainsString('2', $html);
|
||||
$this->assertStringContainsString('Active contacts', $html);
|
||||
$this->assertStringContainsString('1', $html);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\UserBundle\Entity\Permission;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
|
||||
|
||||
final class ListControllerPermissionFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $nonAdminUser;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $userOne;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $userTwo;
|
||||
|
||||
/**
|
||||
* @var LeadList
|
||||
*/
|
||||
private $segmentA;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->nonAdminUser = $this->createUser([
|
||||
'user-name' => 'non-admin',
|
||||
'email' => 'non-admin@mautic-test.com',
|
||||
'first-name' => 'non-admin',
|
||||
'last-name' => 'non-admin',
|
||||
'role' => [
|
||||
'name' => 'perm_non_admin',
|
||||
'perm' => 'core:themes',
|
||||
'bitwise' => 1024,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->userOne = $this->createUser(
|
||||
[
|
||||
'user-name' => 'user-one',
|
||||
'email' => 'user-one@mautic-test.com',
|
||||
'first-name' => 'user-one',
|
||||
'last-name' => 'user-one',
|
||||
'role' => [
|
||||
'name' => 'perm_user_one',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 40,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->userTwo = $this->createUser([
|
||||
'user-name' => 'user-two',
|
||||
'email' => 'user-two@mautic-test.com',
|
||||
'first-name' => 'user-two',
|
||||
'last-name' => 'user-two',
|
||||
'role' => [
|
||||
'name' => 'perm_user_two',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 16,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->segmentA = $this->createSegment('Segment List A', $this->userOne);
|
||||
}
|
||||
|
||||
public function testIndexPageWithCreatePermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertCount(1, $crawler->filterXPath('//a[contains(@href,"/s/segments/new")]'), 'Listing page has the New button');
|
||||
}
|
||||
|
||||
public function testIndexPageNonAdmin(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments');
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testIndexPageForPaging(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/2');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCreateSegmentForUserWithoutPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCreateSegmentForUserWithPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Submitting for cancel button click.
|
||||
$form = $crawler->selectButton('Cancel')->form();
|
||||
$crawlerCancel = $this->client->submit($form);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Contact Segments', $crawlerCancel->html());
|
||||
|
||||
// Save the Segment.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/new');
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[name]']->setValue('Segment Test');
|
||||
$form['leadlist[alias]']->setValue('segment_test');
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Segment - Segment Test', $crawler->html());
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('dataSegmentCloneUserPermissions')]
|
||||
public function testSegmentCloningOwnedSegmentWithDifferentPermissions(string $name, int $perm, int $expected): void
|
||||
{
|
||||
$user = $this->createUser(
|
||||
[
|
||||
'user-name' => $name,
|
||||
'email' => $name.'@mautic-test.com',
|
||||
'first-name' => $name,
|
||||
'last-name' => $name,
|
||||
'role' => [
|
||||
'name' => 'perm_user_three',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => $perm, // Create and View own
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('Test Segment for clone test', $user);
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$segment->getId());
|
||||
$this->assertEquals($expected, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, mixed[]>
|
||||
*/
|
||||
public static function dataSegmentCloneUserPermissions(): iterable
|
||||
{
|
||||
yield 'Only create' => ['user-clone-1', 32, Response::HTTP_FORBIDDEN];
|
||||
yield 'Create and View own' => ['user-clone-2', 34, Response::HTTP_OK];
|
||||
yield 'Create and View other' => ['user-clone-2', 36, Response::HTTP_FORBIDDEN];
|
||||
}
|
||||
|
||||
public function testSegmentCloningUsingUserHavingPermissions(): void
|
||||
{
|
||||
$user = $this->createUser(
|
||||
[
|
||||
'user-name' => 'user-3',
|
||||
'email' => 'user-3@mautic-test.com',
|
||||
'first-name' => 'user-3',
|
||||
'last-name' => 'user-3',
|
||||
'role' => [
|
||||
'name' => 'perm_user_three',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 36, // Create and view other
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testSegmentCloningUsingUserWithoutPermissions(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/clone/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testCloneInvalidSegment(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/clone/2000');
|
||||
// For no entity found it will redirect to index page.
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
$this->assertStringContainsString('No segment with an id of 2000 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testEditSegmentAndClickOnButtons(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Submitting for cancel button click.
|
||||
$form = $crawler->selectButton('Cancel')->form();
|
||||
$crawlerCancel = $this->client->submit($form);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString($this->segmentA->getName(), $crawlerCancel->html());
|
||||
|
||||
// Save the Segment.
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$form = $crawler->selectButton('leadlist_buttons_apply')->form();
|
||||
$form['leadlist[isPublished]']->setValue('0');
|
||||
$crawler = $this->client->submit($form);
|
||||
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('Edit Segment - '.$this->segmentA->getName(), $crawler->html());
|
||||
}
|
||||
|
||||
public function testEditInvalidSegment(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/segments/edit/2000');
|
||||
// For no entity found it will redirect to index page.
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
$this->assertStringContainsString('No segment with an id of 2000 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testEditOwnSegment(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditOthersSegment(): void
|
||||
{
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditSegmentForUserWithoutPermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-edit',
|
||||
'email' => 'user-edit@mautic-test.com',
|
||||
'first-name' => 'user-edit',
|
||||
'last-name' => 'user-edit',
|
||||
'role' => [
|
||||
'name' => 'perm_user_edit',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 8,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testEditSegmentWhileLock(): void
|
||||
{
|
||||
$segmentA = $this->segmentA;
|
||||
$segmentA->setCheckedOut(new \DateTime());
|
||||
$segmentA->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/edit/'.$segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// As $segmentA is locked, so it will redirect user to its view page.
|
||||
$this->assertStringContainsString('/s/segments/view/'.$segmentA->getId(), $this->client->getRequest()->getRequestUri());
|
||||
}
|
||||
|
||||
public function testDeleteSegmentWithoutPermission(): void
|
||||
{
|
||||
$this->loginOtherUser($this->nonAdminUser->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDeleteOthersSegmentWithPermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-other',
|
||||
'email' => 'user-delete-other@mautic-test.com',
|
||||
'first-name' => 'user-delete-other',
|
||||
'last-name' => 'user-delete-other',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_other',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 128,
|
||||
],
|
||||
]);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$this->segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDeleteSegmentWithDependencyAndLockedInWithOtherUser(): void
|
||||
{
|
||||
$listId = $this->segmentA->getId();
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$listId],
|
||||
],
|
||||
'display' => '',
|
||||
'filter' => [$listId],
|
||||
]];
|
||||
$segmentA = $this->createSegment('Segment List A', $this->userTwo, $filter);
|
||||
|
||||
$this->assertSame($filter, $segmentA->getFilters(), 'Filters');
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$listId);
|
||||
$this->assertStringContainsString("Segment cannot be deleted, it is required by {$segmentA->getName()}.", $crawler->text());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$segmentA->setCheckedOut(new \DateTime());
|
||||
$segmentA->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentA);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$segmentA->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString("{$segmentA->getName()} is currently checked out by", $crawler->html());
|
||||
|
||||
// As $segmentA is locked, so it will redirect user to its view page.
|
||||
$this->assertStringContainsString('/s/segments/1', $this->client->getRequest()->getRequestUri());
|
||||
}
|
||||
|
||||
public function testDeleteInvalidSegment(): void
|
||||
{
|
||||
$listId = 99999;
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/delete/'.$listId);
|
||||
$this->assertStringContainsString("No segment with an id of {$listId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserDoNotHavePermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$this->segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The logged-in user do not have permission to delete the segment $this->segmentA.
|
||||
$this->assertStringContainsString('You do not have access to the requested area/action.', $crawler->text());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserDoNotHavePermissionAndSegmentIsInvalid(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
101,
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment 101 is invalid.
|
||||
$this->assertStringContainsString('No segment with an id of 101 was found!', $crawler->text());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenUserHavePermission(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentA = $this->createSegment('Segment List A', $user);
|
||||
$this->em->flush();
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// Only one segments is deleted.
|
||||
$this->assertStringContainsString('1 segments have been deleted!', $crawler->html());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenDeletingLocked(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentC = $this->createSegment('Segment List C', $user);
|
||||
$segmentC->setCheckedOut(new \DateTime());
|
||||
$segmentC->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($segmentC);
|
||||
$this->em->flush();
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentC->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment $segmentC is being locked by user other than logged-in.
|
||||
$this->assertStringContainsString("{$segmentC->getName()} is currently checked out by", $crawler->html());
|
||||
}
|
||||
|
||||
public function testBatchDeleteSegmentWhenDeletingRequiredByOthers(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-delete-a',
|
||||
'email' => 'user-delete-a@mautic-test.com',
|
||||
'first-name' => 'user-delete-a',
|
||||
'last-name' => 'user-delete-a',
|
||||
'role' => [
|
||||
'name' => 'perm_user_delete_a',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 82,
|
||||
],
|
||||
]);
|
||||
|
||||
$segmentA = $this->createSegment('Segment List A', $user);
|
||||
|
||||
$filter = [[
|
||||
'object' => 'lead',
|
||||
'glue' => 'and',
|
||||
'field' => 'leadlist',
|
||||
'type' => 'leadlist',
|
||||
'operator' => 'in',
|
||||
'properties' => [
|
||||
'filter' => [$segmentA->getId()],
|
||||
],
|
||||
'display' => '',
|
||||
'filter' => [$segmentA->getId()],
|
||||
]];
|
||||
|
||||
$segmentB = $this->createSegment('Segment List with filter', $user, $filter);
|
||||
$this->assertSame($filter, $segmentB->getFilters(), 'Filters');
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segmentIds = [
|
||||
$segmentA->getId(),
|
||||
];
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/batchDelete?ids='.json_encode($segmentIds));
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
// The segment $segmentA is used as filter in $segmentB.
|
||||
$this->assertStringContainsString("{$segmentA->getName()} cannot be deleted, it is required by other segments.", $crawler->text());
|
||||
}
|
||||
|
||||
public function testViewSegment(): void
|
||||
{
|
||||
$user = $this->createUser([
|
||||
'user-name' => 'user-view-own',
|
||||
'email' => 'user-view-own@mautic-test.com',
|
||||
'first-name' => 'user-view-own',
|
||||
'last-name' => 'user-view-own',
|
||||
'role' => [
|
||||
'name' => 'perm_user_view_own',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 2,
|
||||
],
|
||||
]);
|
||||
$segment = $this->createSegment('Segment News View', $user);
|
||||
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_GET, '/s/segments/view/'.$segment->getId());
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPostOnViewSegment(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/view/'.$this->segmentA->getId(), [
|
||||
'includeEvents' => [
|
||||
'manually_added',
|
||||
'manually_removed',
|
||||
'filter_added',
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveLeadFromSegmentWhereUserIsNotOwnerOfSegment(): void
|
||||
{
|
||||
$leadId = $this->createLead($this->userOne)->getId();
|
||||
$this->loginOtherUser($this->userTwo->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/removeLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testRemoveLeadFromSegmentWhereUserIsOwnerOfSegment(): void
|
||||
{
|
||||
$leadId = $this->createLead($this->userOne)->getId();
|
||||
$this->loginOtherUser($this->userOne->getUserIdentifier());
|
||||
$this->client->request(Request::METHOD_POST, '/s/segments/removeLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertEquals(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAddLeadToSegmentForInvalidLeadAndLockedLeadAndInvalidSegment(): void
|
||||
{
|
||||
$leadId = 99999;
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$this->segmentA->getId().'?leadId='.$leadId);
|
||||
$this->assertStringContainsString("No contact with an id of {$leadId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$listId = 9999;
|
||||
$lead = $this->createLead($this->userOne);
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$listId.'?leadId='.$lead->getId());
|
||||
$this->assertStringContainsString("No segment with an id of {$listId} was found!", $crawler->html());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$lead->setCheckedOut(new \DateTime());
|
||||
$lead->setCheckedOutBy($this->userOne);
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_POST, '/s/segments/addLead/'.$this->segmentA->getId().'?leadId='.$lead->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString("{$lead->getPrimaryIdentifier()} is currently checked out by", $crawler->html());
|
||||
}
|
||||
|
||||
public function testUserCanPublishOwnSegmentWithPermission(): void
|
||||
{
|
||||
$userDetails = [
|
||||
'user-name' => 'testuser_publish',
|
||||
'first-name' => 'Test',
|
||||
'last-name' => 'Publisher',
|
||||
'email' => 'testuser_publish@example.com',
|
||||
'role' => [
|
||||
'name' => 'Can Publish Own Segments',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 382, // View own&other, Edit own&other, Create, Delete One, Publish Own
|
||||
],
|
||||
];
|
||||
|
||||
$user = $this->createUser($userDetails);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('My Segment to Publish', $user);
|
||||
|
||||
$this->assertTrue($segment->isPublished());
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'togglePublishStatus',
|
||||
'model' => 'lead.list',
|
||||
'id' => $segment->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->em->refresh($segment);
|
||||
$this->assertFalse($segment->isPublished());
|
||||
}
|
||||
|
||||
public function testUserCannotPublishOwnSegmentWithoutPermission(): void
|
||||
{
|
||||
$userDetails = [
|
||||
'user-name' => 'testuser_nopublish',
|
||||
'first-name' => 'Test',
|
||||
'last-name' => 'NoPublisher',
|
||||
'email' => 'testuser_nopublish@example.com',
|
||||
'role' => [
|
||||
'name' => 'No Publish Permission',
|
||||
'perm' => 'lead:lists',
|
||||
'bitwise' => 126, // View own&other, Edit own&other, Create, Delete One
|
||||
],
|
||||
];
|
||||
|
||||
$user = $this->createUser($userDetails);
|
||||
$this->loginOtherUser($user->getUserIdentifier());
|
||||
|
||||
$segment = $this->createSegment('Segment Without Publish', $user);
|
||||
|
||||
$this->client->request(
|
||||
'POST',
|
||||
'/s/ajax',
|
||||
[
|
||||
'action' => 'togglePublishStatus',
|
||||
'model' => 'lead.list',
|
||||
'id' => $segment->getId(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(403, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$this->em->refresh($segment);
|
||||
$this->assertTrue($segment->isPublished());
|
||||
}
|
||||
|
||||
private function loginOtherUser(string $name): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_GET, '/s/logout');
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => $name]);
|
||||
|
||||
$this->loginUser($user);
|
||||
$this->client->setServerParameter('PHP_AUTH_USER', $name);
|
||||
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $userDetails
|
||||
*/
|
||||
private function createUser(array $userDetails): User
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setName($userDetails['role']['name']);
|
||||
$role->setIsAdmin(false);
|
||||
|
||||
$this->em->persist($role);
|
||||
|
||||
$this->createPermission($role, $userDetails['role']['perm'], $userDetails['role']['bitwise']);
|
||||
|
||||
$user = new User();
|
||||
$user->setEmail($userDetails['email']);
|
||||
$user->setUsername($userDetails['user-name']);
|
||||
$user->setFirstName($userDetails['first-name']);
|
||||
$user->setLastName($userDetails['last-name']);
|
||||
$user->setRole($role);
|
||||
|
||||
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
|
||||
\assert($hasher instanceof PasswordHasherInterface);
|
||||
$user->setPassword($hasher->hash('Maut1cR0cks!'));
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function createPermission(Role $role, string $rawPermission, int $bitwise): void
|
||||
{
|
||||
$parts = explode(':', $rawPermission);
|
||||
$permission = new Permission();
|
||||
$permission->setBundle($parts[0]);
|
||||
$permission->setName($parts[1]);
|
||||
$permission->setRole($role);
|
||||
$permission->setBitwise($bitwise);
|
||||
$this->em->persist($permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*/
|
||||
private function createSegment(string $name, User $user, array $filters = []): LeadList
|
||||
{
|
||||
$segment = new LeadList();
|
||||
$segment->setName($name);
|
||||
$segment->setPublicName($name);
|
||||
$segment->setAlias(str_shuffle('abcdefghijklmnopqrstuvwxyz'));
|
||||
$segment->setCreatedBy($user);
|
||||
|
||||
if ($filters) {
|
||||
$segment->setFilters($filters);
|
||||
}
|
||||
|
||||
$this->em->persist($segment);
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function createLead(User $user): Lead
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setCreatedByUser($user);
|
||||
|
||||
$this->em->persist($lead);
|
||||
$this->em->flush();
|
||||
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Traits\ControllerTrait;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Entity\ListLead;
|
||||
|
||||
class ListControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use ControllerTrait;
|
||||
|
||||
/**
|
||||
* Index action should return status code 200.
|
||||
*/
|
||||
public function testIndexAction(): void
|
||||
{
|
||||
$list = $this->createList();
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$urlAlias = 'segments';
|
||||
$routeAlias = 'leadlist';
|
||||
$column = 'dateModified';
|
||||
$column2 = 'name';
|
||||
$tableAlias = 'l.';
|
||||
|
||||
$this->getControllerColumnTests($urlAlias, $routeAlias, $column, $tableAlias, $column2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if list contains correct values.
|
||||
*/
|
||||
public function testViewList(): void
|
||||
{
|
||||
$list = $this->createList();
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->request('GET', '/s/segments');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
$this->assertStringContainsString('February 7, 2020', $clientResponse->getContent());
|
||||
$this->assertStringContainsString('March 21, 2020', $clientResponse->getContent());
|
||||
$this->assertStringContainsString('Test User', $clientResponse->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering should return status code 200.
|
||||
*/
|
||||
public function testIndexActionWhenFiltering(): void
|
||||
{
|
||||
$this->client->request('GET', '/s/segments?search=has%3Aresults&tmpl=list');
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
}
|
||||
|
||||
public function testSegmentView(): void
|
||||
{
|
||||
$contacts = $this->createContacts();
|
||||
$segment = $this->addContactsToSegment($contacts, 'MySeg');
|
||||
$this->client->request('GET', sprintf('/s/segments/view/%d', $segment->getId()));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('MySeg', $response->getContent());
|
||||
// Make sure that contact grid is not loaded synchronously
|
||||
self::assertStringNotContainsString('Kane', $response->getContent());
|
||||
self::assertStringNotContainsString('Jacques', $response->getContent());
|
||||
// Make sure the data-target-url is not an absolute URL
|
||||
self::assertStringContainsString(sprintf('data-target-url="/s/segment/view/%s/contact/1"', $segment->getId()), $response->getContent());
|
||||
}
|
||||
|
||||
public function testSegmentContactGrid(): void
|
||||
{
|
||||
$pageId = 1;
|
||||
$contacts = $this->createContacts();
|
||||
$segment = $this->addContactsToSegment($contacts, 'MySeg');
|
||||
$this->client->request('GET', sprintf('/s/segment/view/%d/contact/%d', $segment->getId(), $pageId));
|
||||
$response = $this->client->getResponse();
|
||||
self::assertResponseIsSuccessful();
|
||||
self::assertStringContainsString('Kane', $response->getContent());
|
||||
self::assertStringContainsString('Jacques', $response->getContent());
|
||||
}
|
||||
|
||||
private function createList(string $suffix = 'A'): LeadList
|
||||
{
|
||||
$list = new LeadList();
|
||||
$list->setName("Segment $suffix");
|
||||
$list->setPublicName("Segment $suffix");
|
||||
$list->setAlias("segment-$suffix");
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead[]
|
||||
*/
|
||||
private function createContacts(): array
|
||||
{
|
||||
$contact1 = new Lead();
|
||||
$contact1->setFirstname('Kane');
|
||||
$contact1->setLastname('Williamson');
|
||||
$contact1->setEmail('kane.williamson@test.com');
|
||||
|
||||
$contact2 = new Lead();
|
||||
$contact2->setFirstname('Jacques');
|
||||
$contact2->setLastname('Kallis');
|
||||
$contact2->setEmail('jacques.kallis@test.com');
|
||||
|
||||
$this->em->persist($contact1);
|
||||
$this->em->persist($contact2);
|
||||
$this->em->flush();
|
||||
|
||||
return [$contact1, $contact2];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead[] $contacts
|
||||
*/
|
||||
private function addContactsToSegment(array $contacts, string $segmentName): LeadList
|
||||
{
|
||||
$filters = [
|
||||
[
|
||||
'glue' => 'and',
|
||||
'field' => 'company',
|
||||
'object' => 'lead',
|
||||
'type' => 'text',
|
||||
'operator' => 'contains',
|
||||
'properties' => [
|
||||
'filter' => 'Acquia',
|
||||
],
|
||||
'filter' => 'Acquia',
|
||||
'display' => null,
|
||||
],
|
||||
];
|
||||
|
||||
$segment = new LeadList();
|
||||
$segment->setName($segmentName);
|
||||
$segment->setPublicName($segmentName);
|
||||
$segment->setAlias(strtolower($segmentName));
|
||||
$segment->isPublished(true);
|
||||
$segment->setDateAdded(new \DateTime());
|
||||
$segment->setFilters($filters);
|
||||
$segment->setIsGlobal(true);
|
||||
$segment->setIsPreferenceCenter(false);
|
||||
$this->em->persist($segment);
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
$segmentContacts = new ListLead();
|
||||
$segmentContacts->setList($segment);
|
||||
$segmentContacts->setLead($contact);
|
||||
$segmentContacts->setDateAdded(new \DateTime());
|
||||
$segmentContacts->setManuallyAdded(false);
|
||||
$segmentContacts->setManuallyRemoved(false);
|
||||
$this->em->persist($segmentContacts);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $segment;
|
||||
}
|
||||
|
||||
public function testCloneSegmentPage(): void
|
||||
{
|
||||
$list = $this->createList('clone');
|
||||
$list->setDateAdded(new \DateTime('2020-02-07 20:29:02'));
|
||||
$list->setDateModified(new \DateTime('2020-03-21 20:29:02'));
|
||||
$list->setCreatedByUser('Test User');
|
||||
|
||||
$this->em->persist($list);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->client->request('GET', sprintf('/s/segments/clone/%d', $list->getId()));
|
||||
|
||||
$clientResponse = $this->client->getResponse();
|
||||
$this->assertResponseIsSuccessful('Return code must be 200.');
|
||||
self::assertStringContainsString('Segment clone', $clientResponse->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class NoteControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function beforeBeginTransaction(): void
|
||||
{
|
||||
$this->resetAutoincrement([
|
||||
'leads',
|
||||
'companies',
|
||||
'campaigns',
|
||||
'categories',
|
||||
'lead_lists',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick smoke test to ensure the route is successful.
|
||||
*/
|
||||
public function testIndexActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = (new Lead())->setFirstname('Test');
|
||||
static::getContainer()->get('mautic.lead.model.lead')->saveEntity($contact);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/contacts/notes/'.$contact->getId());
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick smoke test to ensure the route is successful.
|
||||
*/
|
||||
public function testNewActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = (new Lead())->setFirstname('Test');
|
||||
static::getContainer()->get('mautic.lead.model.lead')->saveEntity($contact);
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/contacts/notes/'.$contact->getId().'/new');
|
||||
$this->assertEquals(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Tests\Controller;
|
||||
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\CoreBundle\Tests\Functional\CreateTestEntitiesTrait;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class TimelineControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateTestEntitiesTrait;
|
||||
private const SALES_USER = 'sales';
|
||||
|
||||
public function testIndexActionsIsSuccessful(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/timeline/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testFilterCaseInsensitive(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$segment = $this->createSegment('TEST', []);
|
||||
$this->createListLead($segment, $contact);
|
||||
$this->em->flush();
|
||||
$this->createLeadEventLogEntry($contact, 'lead', 'segment', 'added', $segment->getId(), [
|
||||
'object_description' => $segment->getName(),
|
||||
]);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('POST', '/s/contacts/timeline/'.$contact->getId(), [
|
||||
'search' => 'test',
|
||||
'leadId' => $contact->getId(),
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString('Contact added to segment, TEST', $this->client->getResponse()->getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws OptimisticLockException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function testBatchExportActionAsAdmin(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$this->client->request('GET', '/s/contacts/timeline/batchExport/'.$contact->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testBatchExportActionAsUserNotPermission(): void
|
||||
{
|
||||
$contact = $this->createLead('TestFirstName');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$user = $this->em->getRepository(User::class)->findOneBy(['username' => self::SALES_USER]);
|
||||
$this->loginUser($user);
|
||||
$this->client->request('GET', '/s/contacts/timeline/batchExport/'.$contact->getId());
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user