Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collection;
use Mautic\FormBundle\Collection\FieldCollection;
use Mautic\FormBundle\Crate\FieldCrate;
use Mautic\FormBundle\Exception\FieldNotFoundException;
final class FieldCollectionTest extends \PHPUnit\Framework\TestCase
{
public function testToChoicesWithObjects(): void
{
$collection = new FieldCollection(
[
new FieldCrate('6', 'email', 'email', []),
new FieldCrate('7', 'first_name', 'text', []),
]
);
$this->assertSame(
[
'email' => '6',
'first_name' => '7',
],
$collection->toChoices()
);
}
public function testToChoicesWithoutObjects(): void
{
$collection = new FieldCollection();
$this->assertSame([], $collection->toChoices());
}
public function testGetFieldByKey(): void
{
$field6 = new FieldCrate('6', 'email', 'email', []);
$field7 = new FieldCrate('7', 'first_name', 'text', []);
$collection = new FieldCollection([$field6, $field7]);
$this->assertSame($field6, $collection->getFieldByKey('6'));
$this->assertSame($field7, $collection->getFieldByKey('7'));
$this->expectException(FieldNotFoundException::class);
$collection->getFieldByKey('8');
}
public function testRemoveFieldsWithKeysWithNoKeyToKeep(): void
{
$field6 = new FieldCrate('6', 'email', 'email', []);
$field7 = new FieldCrate('7', 'first_name', 'text', []);
$field8 = new FieldCrate('8', 'last_name', 'text', []);
$originalCollection = new FieldCollection([$field6, $field7, $field8]);
$resultCollection = $originalCollection->removeFieldsWithKeys(['6', '8']);
// It should return a clone of the original collection. Not mutation.
$this->assertNotSame($originalCollection, $resultCollection);
$this->assertCount(1, $resultCollection);
$this->assertSame($field7, $resultCollection->getFieldByKey('7'));
}
public function testRemoveFieldsWithKeysWithKeyToKeep(): void
{
$field6 = new FieldCrate('6', 'email', 'email', []);
$field7 = new FieldCrate('7', 'first_name', 'text', []);
$field8 = new FieldCrate('8', 'last_name', 'text', []);
$originalCollection = new FieldCollection([$field6, $field7, $field8]);
$resultCollection = $originalCollection->removeFieldsWithKeys(['6', '8'], '8');
$this->assertCount(2, $resultCollection);
$this->assertSame($field7, $resultCollection->getFieldByKey('7'));
$this->assertSame($field8, $resultCollection->getFieldByKey('8'));
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collection;
use Mautic\FormBundle\Collection\ObjectCollection;
use Mautic\FormBundle\Crate\ObjectCrate;
final class ObjectCollectionTest extends \PHPUnit\Framework\TestCase
{
public function testToChoicesWithObjects(): void
{
$collection = new ObjectCollection(
[
new ObjectCrate('contact', 'Contact'),
new ObjectCrate('company', 'Company'),
]
);
$this->assertSame(
[
'Contact' => 'contact',
'Company' => 'company',
],
$collection->toChoices()
);
}
public function testToChoicesWithoutObjects(): void
{
$collection = new ObjectCollection();
$this->assertSame([], $collection->toChoices());
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collector;
use Mautic\CacheBundle\Cache\CacheProviderTagAwareInterface;
use Mautic\FormBundle\Collector\AlreadyMappedFieldCollector;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Cache\CacheItem;
final class AlreadyMappedFieldCollectorTest extends \PHPUnit\Framework\TestCase
{
private MockObject&CacheProviderTagAwareInterface $cacheProvider;
private AlreadyMappedFieldCollector $collector;
protected function setup(): void
{
parent::setUp();
$this->cacheProvider = $this->createMock(CacheProviderTagAwareInterface::class);
$this->collector = new AlreadyMappedFieldCollector($this->cacheProvider);
}
public function testWorkflow(): void
{
$createCacheItem = \Closure::bind(
function () {
$item = new CacheItem();
$item->isHit = false;
$item->isTaggable = true;
return $item;
},
$this,
CacheItem::class
);
$cacheItem = $createCacheItem();
$formId = '3';
$object = 'contact';
$this->cacheProvider->method('getItem')
->with('mautic.form.3.object.contact.fields.mapped')
->willReturn($cacheItem);
$this->cacheProvider->expects($this->exactly(4))
->method('save')
->with($cacheItem);
// Ensure we get an empty array at the beginning.
$this->assertNull($cacheItem->get());
$this->assertSame([], $this->collector->getFields($formId, $object));
// Add a mapped field.
$this->collector->addField('3', 'contact', '44');
$this->assertSame(['44'], $this->collector->getFields($formId, $object));
// The field with key 44 should be added to the cache item.
$this->assertSame('["44"]', $cacheItem->get());
// Add another mapped field.
$this->collector->addField('3', 'contact', '55');
// The field with key 55 should be added to the cache item.
$this->assertSame('["44","55"]', $cacheItem->get());
$this->assertSame(['44', '55'], $this->collector->getFields($formId, $object));
// Remove an exsting field.
$this->collector->removeField('3', 'contact', '44');
// The field with key 44 should be removed from the cache item.
$this->assertSame('["55"]', $cacheItem->get());
$this->assertSame(['55'], $this->collector->getFields($formId, $object));
// Remove a not exsting field.
$this->collector->removeField('3', 'contact', '44');
// Still the same result after removing a field that did not exist.
$this->assertSame('["55"]', $cacheItem->get());
$this->assertSame(['55'], $this->collector->getFields($formId, $object));
$this->cacheProvider->expects($this->once())
->method('invalidateTags')
->with(['mautic.form.3.fields.mapped']);
$this->collector->removeAllForForm($formId);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collector;
use Mautic\FormBundle\Collection\FieldCollection;
use Mautic\FormBundle\Collector\FieldCollector;
use Mautic\FormBundle\Event\FieldCollectEvent;
use PHPUnit\Framework\Assert;
use Symfony\Component\EventDispatcher\EventDispatcher;
final class FieldCollectorTest extends \PHPUnit\Framework\TestCase
{
public function testBuildCollectionForNoObject(): void
{
$dispatcher = new class extends EventDispatcher {
public int $dispatchMethodCallCounter = 0;
public function dispatch(object $event, ?string $eventName = null): object
{
++$this->dispatchMethodCallCounter;
\assert($event instanceof FieldCollectEvent);
Assert::assertSame('contact', $event->getObject());
return new FieldCollection();
}
};
$fieldCollector = new FieldCollector($dispatcher);
$fieldCollector->getFields('contact');
// Calling for the second time to ensure it's cached and the dispatcher is called only once.
$fieldCollection = $fieldCollector->getFields('contact');
Assert::assertEquals(1, $dispatcher->dispatchMethodCallCounter);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collector;
use Mautic\FormBundle\Collection\FieldCollection;
use Mautic\FormBundle\Collector\FieldCollectorInterface;
use Mautic\FormBundle\Collector\MappedObjectCollector;
use PHPUnit\Framework\Assert;
final class MappedObjectCollectorTest extends \PHPUnit\Framework\TestCase
{
public function testBuildCollectionForNoObject(): void
{
$fieldCollector = new class implements FieldCollectorInterface {
public int $getFieldsMethodCallCounter = 0;
public function getFields(string $object): FieldCollection
{
++$this->getFieldsMethodCallCounter;
return new FieldCollection();
}
};
$mappedObjectCollector = new MappedObjectCollector($fieldCollector);
$objectCollection = $mappedObjectCollector->buildCollection('');
Assert::assertCount(0, $objectCollection);
Assert::assertEquals(0, $fieldCollector->getFieldsMethodCallCounter);
}
public function testBuildCollectionForOneObject(): void
{
$fieldCollector = new class implements FieldCollectorInterface {
public int $getFieldsMethodCallCounter = 0;
public function getFields(string $object): FieldCollection
{
Assert::assertSame($object, 'contact');
++$this->getFieldsMethodCallCounter;
return new FieldCollection();
}
};
$mappedObjectCollector = new MappedObjectCollector($fieldCollector);
$objectCollection = $mappedObjectCollector->buildCollection('contact');
Assert::assertCount(1, $objectCollection);
Assert::assertEquals(1, $fieldCollector->getFieldsMethodCallCounter);
}
public function testBuildCollectionForMultipleObjects(): void
{
$fieldCollector = new class implements FieldCollectorInterface {
public int $getFieldsMethodCallCounter = 0;
public function getFields(string $object): FieldCollection
{
Assert::assertContains($object, ['company', 'contact']);
++$this->getFieldsMethodCallCounter;
return new FieldCollection();
}
};
$mappedObjectCollector = new MappedObjectCollector($fieldCollector);
$objectCollection = $mappedObjectCollector->buildCollection('contact', 'company');
Assert::assertCount(2, $objectCollection);
Assert::assertEquals(2, $fieldCollector->getFieldsMethodCallCounter);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Collector;
use Mautic\FormBundle\Collection\ObjectCollection;
use Mautic\FormBundle\Collector\ObjectCollector;
use Mautic\FormBundle\Event\ObjectCollectEvent;
use PHPUnit\Framework\Assert;
use Symfony\Component\EventDispatcher\EventDispatcher;
final class ObjectCollectorTest extends \PHPUnit\Framework\TestCase
{
public function testBuildCollectionForNoObject(): void
{
$dispatcher = new class extends EventDispatcher {
public int $dispatchMethodCallCounter = 0;
public function dispatch(object $event, ?string $eventName = null): object
{
++$this->dispatchMethodCallCounter;
Assert::assertInstanceOf(ObjectCollectEvent::class, $event);
return new ObjectCollection();
}
};
$objectCollector = new ObjectCollector($dispatcher);
$objectCollector->getObjects();
// Calling for the second time to ensure it's cached and the dispatcher is called only once.
$objectCollection = $objectCollector->getObjects();
Assert::assertEquals(1, $dispatcher->dispatchMethodCallCounter);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Form;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
final class ActionControllerFunctionalTest extends MauticMysqlTestCase
{
/**
* @throws \Doctrine\ORM\OptimisticLockException
* @throws \Doctrine\ORM\ORMException
*/
public function testNewActionWithJapanese(): void
{
// Create new form
$form = new Form();
$form->setName('Test Form');
$form->setAlias('testform');
$this->em->persist($form);
$this->em->flush();
// Fetch the form
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/forms/action/new',
[
'formId' => $form->getId(),
'type' => 'form.email',
]
);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formCrawler = $crawler->filter('form');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
// Save new Send Form Results action
$form->setValues([
'formaction[properties][subject]' => 'Test Japanese',
'formaction[properties][message]' => '<p style="font-family: メイリオ">Test</p>',
]);
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$actionHtml = json_decode($content, true)['actionHtml'] ?? null;
Assert::assertNotNull($actionHtml, $content);
$crawler = new Crawler($actionHtml);
$editPage = $crawler->filter('.btn-edit')->attr('href');
// Check the content was not changed
$this->client->xmlHttpRequest(Request::METHOD_GET, $editPage);
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('&lt;p style=&quot;font-family: メイリオ&quot;&gt;Test&lt;/p&gt;', json_decode($this->client->getResponse()->getContent())->newContent);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
final class AjaxControllerFunctionalTest extends MauticMysqlTestCase
{
public function testGetFieldsForObjectAction(): void
{
$this->client->xmlHttpRequest(
Request::METHOD_GET,
'/s/ajax?action=form:getFieldsForObject&mappedObject=company&mappedField=&formId=10'
);
$clientResponse = $this->client->getResponse();
$payload = json_decode($clientResponse->getContent(), true);
self::assertResponseIsSuccessful();
// Assert some random fields exist.
Assert::assertSame(
[
'label' => 'Company Email',
'value' => 'companyemail',
'isListType' => false,
],
$payload['fields'][4]
);
Assert::assertSame(
[
'label' => 'Industry',
'value' => 'companyindustry',
'isListType' => true,
],
$payload['fields'][9]
);
}
}

View File

@@ -0,0 +1,643 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller\Api;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Submission;
use Mautic\LeadBundle\Entity\Company;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class FormApiControllerFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
private const TEST_PAYLOAD = [
'name' => 'API form',
'description' => 'Form created via API test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'text',
'alias' => 'email',
'mappedObject' => 'contact',
'mappedField' => 'email',
'showLabel' => true,
'isRequired' => true,
],
[
'label' => 'Number',
'type' => 'number',
'alias' => 'number',
'leadField' => 'points', // @deprecated Setting leadField, no mappedField or mappedObject (BC).
],
[
'label' => 'Company',
'type' => 'text',
'alias' => 'company',
'leadField' => 'company', // @deprecated Setting leadField, no mappedField or mappedObject (BC).
],
[
'label' => 'Company Phone',
'type' => 'tel',
'alias' => 'phone',
'leadField' => 'companyphone', // @deprecated Setting leadField, no mappedField or mappedObject (BC).
],
[
'label' => 'Country',
'type' => 'country',
'alias' => 'country',
'mappedObject' => 'contact',
'mappedField' => 'country',
],
[
'label' => 'Multiselect',
'type' => 'select',
'alias' => 'multiselect',
'properties' => [
'syncList' => 0,
'multiple' => 1,
'list' => [
'list' => [
[
'label' => 'One',
'value' => 'one',
],
[
'label' => 'Two',
'value' => 'two',
],
],
],
],
],
[
'label' => 'Submit',
'type' => 'button',
],
],
'actions' => [
],
'postAction' => 'return',
];
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $expectedResponse
*/
#[\PHPUnit\Framework\Attributes\DataProvider('formDataProvider')]
public function testAddAndEditForms(array $payload, array $expectedResponse): void
{
$this->client->request('POST', '/api/forms/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']);
}
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, 'Return code must be 201.');
$formId = $response['form']['id'];
$this->assertGreaterThan(0, $formId);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertEquals($payload['isPublished'], $response['form']['isPublished']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertIsArray($response['form']['fields']);
$this->assertCount(count($payload['fields']), $response['form']['fields']);
for ($i = 0; $i < count($payload['fields']); ++$i) {
$this->assertEquals($payload['fields'][$i]['label'], $response['form']['fields'][$i]['label']);
$this->assertEquals($payload['fields'][$i]['alias'], $response['form']['fields'][$i]['alias']);
$this->assertEquals($payload['fields'][$i]['type'], $response['form']['fields'][$i]['type']);
$this->assertEquals($expectedResponse['fields'][$i]['leadField'], $response['form']['fields'][$i]['leadField']);
$this->assertEquals($expectedResponse['fields'][$i]['mappedField'], $response['form']['fields'][$i]['mappedField']);
$this->assertEquals($expectedResponse['fields'][$i]['mappedObject'], $response['form']['fields'][$i]['mappedObject']);
}
// Edit PATCH:
$this->client->request('PATCH', "/api/forms/{$formId}/edit", ['name' => $expectedResponse['newName']]);
$clientResponse = $this->client->getResponse();
$responsePatch = json_decode($clientResponse->getContent(), true);
$this->assertResponseIsSuccessful();
$this->assertSame($formId, $responsePatch['form']['id'], 'ID of the created form does not match with the edited one.');
$this->assertEquals($expectedResponse['newName'], $responsePatch['form']['name']);
$this->assertEquals($payload['formType'], $responsePatch['form']['formType']);
$this->assertEquals($payload['isPublished'], $responsePatch['form']['isPublished']);
$this->assertEquals($payload['description'], $responsePatch['form']['description']);
$this->assertIsArray($responsePatch['form']['fields']);
$this->assertCount(count($payload['fields']), $responsePatch['form']['fields']);
for ($i = 0; $i < count($payload['fields']); ++$i) {
$this->assertEquals($payload['fields'][$i]['label'], $responsePatch['form']['fields'][$i]['label']);
$this->assertEquals($payload['fields'][$i]['alias'], $responsePatch['form']['fields'][$i]['alias']);
$this->assertEquals($payload['fields'][$i]['type'], $responsePatch['form']['fields'][$i]['type']);
$this->assertEquals($expectedResponse['fields'][$i]['leadField'], $responsePatch['form']['fields'][$i]['leadField']);
$this->assertEquals($expectedResponse['fields'][$i]['mappedField'], $responsePatch['form']['fields'][$i]['mappedField']);
$this->assertEquals($expectedResponse['fields'][$i]['mappedObject'], $responsePatch['form']['fields'][$i]['mappedObject']);
}
}
/**
* @return array<int, array<int, array<string, mixed>>>
*/
public static function formDataProvider(): array
{
return [
[
[
'name' => 'Form API test',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test',
'fields' => [
[
'label' => 'Email',
'alias' => 'email',
'type' => 'text',
'leadField' => 'email',
],
[
'label' => 'Company Address',
'type' => 'text',
'alias' => 'companyaddress1',
'leadField' => 'companyaddress1',
],
[
'label' => 'Company Phone',
'type' => 'tel',
'alias' => 'phone',
'leadField' => 'companyphone',
],
[
'label' => 'Country',
'type' => 'country',
'alias' => 'country',
'mappedObject' => 'contact',
'mappedField' => 'country',
],
],
'postAction' => 'return',
],
[
'newName' => 'Form API test',
'fields' => [
[
'mappedObject' => 'contact',
'leadField' => 'email',
'mappedField' => 'email',
],
[
'mappedObject' => 'company',
'leadField' => 'companyaddress1',
'mappedField' => 'companyaddress1',
],
[
'mappedObject' => 'company',
'leadField' => 'companyphone',
'mappedField' => 'companyphone',
], [
'mappedObject' => 'contact',
'leadField' => 'country',
'mappedField' => 'country',
],
],
],
],
[
[
'name' => 'Form',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test2',
'fields' => [
[
'label' => 'Lastname',
'alias' => 'lastname',
'type' => 'text',
'mappedField' => 'lastname',
'mappedObject' => 'contact',
],
[
'label' => 'Company Email',
'type' => 'text',
'alias' => 'companyemail1',
'mappedField' => 'companyemail',
'mappedObject' => 'company',
'leadField' => 'companyemail',
],
[
'label' => 'Phone',
'type' => 'tel',
'alias' => 'phone',
'leadField' => 'position',
],
],
'postAction' => 'return',
],
[
'newName' => 'Form API test',
'fields' => [
[
'mappedObject' => 'contact',
'leadField' => 'lastname',
'mappedField' => 'lastname',
],
[
'mappedObject' => 'company',
'leadField' => 'companyemail',
'mappedField' => 'companyemail',
],
[
'mappedObject' => 'contact',
'leadField' => 'position',
'mappedField' => 'position',
],
],
],
],
];
}
public function testSingleFormWorkflow(): void
{
$payload = self::TEST_PAYLOAD;
$fieldCount = count($payload['fields']);
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
Assert::assertTrue(isset($response['form']['id']), $clientResponse->getContent());
$formId = $response['form']['id'];
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, $clientResponse->getContent());
$this->assertGreaterThan(0, $formId);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertNotEmpty($response['form']['cachedHtml']);
$this->assertCount($fieldCount, $response['form']['fields']);
$this->assertEquals($payload['fields'][0]['label'], $response['form']['fields'][0]['label']);
$this->assertEquals($payload['fields'][0]['type'], $response['form']['fields'][0]['type']);
$this->assertEquals($payload['fields'][0]['mappedObject'], $response['form']['fields'][0]['mappedObject']);
$this->assertEquals($payload['fields'][0]['mappedField'], $response['form']['fields'][0]['mappedField']);
$this->assertEquals(
$payload['fields'][0]['mappedField'],
$response['form']['fields'][0]['leadField']
); // @deprecated leadField was replaced by mappedField. Check for BC.
$this->assertEquals($payload['fields'][0]['showLabel'], $response['form']['fields'][0]['showLabel']);
$this->assertEquals($payload['fields'][0]['isRequired'], $response['form']['fields'][0]['isRequired']);
$this->assertEquals($payload['fields'][1]['label'], $response['form']['fields'][1]['label']);
$this->assertEquals($payload['fields'][1]['type'], $response['form']['fields'][1]['type']);
$this->assertEquals('contact', $response['form']['fields'][1]['mappedObject']);
$this->assertEquals('points', $response['form']['fields'][1]['mappedField']);
$this->assertEquals(
$payload['fields'][1]['leadField'],
$response['form']['fields'][1]['leadField']
); // @deprecated leadField was replaced by mappedField. Check for BC.
$this->assertTrue($response['form']['fields'][1]['showLabel']);
$this->assertFalse($response['form']['fields'][1]['isRequired']);
$this->assertEquals($payload['fields'][2]['label'], $response['form']['fields'][2]['label']);
$this->assertEquals($payload['fields'][2]['type'], $response['form']['fields'][2]['type']);
$this->assertEquals('contact', $response['form']['fields'][2]['mappedObject']);
$this->assertEquals('company', $response['form']['fields'][2]['mappedField']);
$this->assertEquals(
$payload['fields'][2]['leadField'],
$response['form']['fields'][2]['leadField']
); // @deprecated leadField was replaced by mappedField. Check for BC.
$this->assertEquals($payload['fields'][3]['label'], $response['form']['fields'][3]['label']);
$this->assertEquals($payload['fields'][3]['type'], $response['form']['fields'][3]['type']);
$this->assertEquals('company', $response['form']['fields'][3]['mappedObject']);
$this->assertEquals('companyphone', $response['form']['fields'][3]['mappedField']);
$this->assertEquals(
$payload['fields'][3]['leadField'],
$response['form']['fields'][3]['leadField']
); // @deprecated leadField was replaced by mappedField. Check for BC.
// Edit PATCH:
$patchPayload = [
'name' => 'API form renamed',
'fields' => [
[
'label' => 'State',
'type' => 'select',
'alias' => 'state',
'mappedObject' => 'contact',
'mappedField' => 'state',
'parent' => $response['form']['fields'][4]['id'],
'conditions' => [
'expr' => 'in',
'any' => 1,
'values' => [],
],
'properties' => [
'syncList' => 1,
'multiple' => 0,
],
],
],
];
$this->client->request(Request::METHOD_PATCH, "/api/forms/{$formId}/edit", $patchPayload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$fieldCount = $fieldCount + 1;
$this->assertResponseIsSuccessful($clientResponse->getContent());
$this->assertSame($formId, $response['form']['id']);
$this->assertEquals('API form renamed', $response['form']['name']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertCount($fieldCount, $response['form']['fields']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertNotEmpty($response['form']['cachedHtml']);
// Edit PUT:
$payload['description'] .= ' renamed';
$payload['fields'] = []; // Set fields to an empty array as it would duplicate all fields.
$payload['postAction'] = 'return'; // Must be present for PUT as all empty values are being cleared.
$this->client->request(Request::METHOD_PUT, "/api/forms/{$formId}/edit", $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertResponseIsSuccessful($clientResponse->getContent());
$this->assertSame($formId, $response['form']['id']);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals('Form created via API test renamed', $response['form']['description']);
$this->assertCount($fieldCount, $response['form']['fields']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertNotEmpty($response['form']['cachedHtml']);
// Get:
$this->client->request(Request::METHOD_GET, "/api/forms/{$formId}");
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertResponseIsSuccessful($clientResponse->getContent());
$this->assertSame($formId, $response['form']['id']);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertCount($fieldCount, $response['form']['fields']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertNotEmpty($response['form']['cachedHtml']);
// Submit the form:
$crawler = $this->client->request(Request::METHOD_GET, "/form/{$formId}");
$formCrawler = $crawler->filter('form[id=mauticform_apiform]');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
$form->setValues([
'mauticform[email]' => 'john@doe.test',
'mauticform[number]' => '123',
'mauticform[company]' => 'Doe Corp',
'mauticform[phone]' => '+420444555666',
'mauticform[country]' => 'Czech Republic',
'mauticform[state]' => 'Plzeňský kraj',
'mauticform[multiselect]' => ['two'],
]);
$this->client->submit($form);
// Ensure the submission was created properly.
$submissions = $this->em->getRepository(Submission::class)->findAll();
Assert::assertCount(1, $submissions);
/** @var Submission $submission */
$submission = $submissions[0];
Assert::assertSame([
'email' => 'john@doe.test',
'number' => 123.0,
'company' => 'Doe Corp',
'phone' => '+420444555666',
'country' => 'Czech Republic',
'multiselect' => 'two',
'state' => 'Plzeňský kraj',
], $submission->getResults());
// A contact should be created by the submission.
$contact = $submission->getLead();
Assert::assertSame('john@doe.test', $contact->getEmail());
Assert::assertSame('Czech Republic', $contact->getCountry());
Assert::assertSame('Plzeňský kraj', $contact->getState());
Assert::assertSame(123, $contact->getPoints());
Assert::assertSame('Doe Corp', $contact->getCompany());
$companies = $this->em->getRepository(Company::class)->findAll();
Assert::assertCount(1, $companies);
// A company should be created by the submission.
/** @var Company $company */
$company = $companies[0];
Assert::assertSame('Doe Corp', $company->getName());
Assert::assertSame('+420444555666', $company->getPhone());
// The previous request changes user to anonymous.
$this->loginUser($this->em->getRepository(User::class)->findOneBy(['username' => 'admin']));
// Delete:
$this->client->request(Request::METHOD_DELETE, "/api/forms/{$formId}/delete");
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertResponseIsSuccessful($clientResponse->getContent());
$this->assertNull($response['form']['id']);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertCount($fieldCount, $response['form']['fields']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertNotEmpty($response['form']['cachedHtml']);
// Get (ensure that the form is gone):
$this->client->request(Request::METHOD_GET, "/api/forms/{$formId}");
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND, $clientResponse->getContent());
$this->assertSame(Response::HTTP_NOT_FOUND, $response['errors'][0]['code']);
}
public function testFormWithChangeTagsAction(): void
{
// Create tag:
$tag1Payload = ['tag' => 'add this'];
$tag2Payload = ['tag' => 'remove this'];
$this->client->request('POST', '/api/tags/new', $tag1Payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$tag1Id = $response['tag']['id'];
$this->client->request('POST', '/api/tags/new', $tag2Payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$tag2Id = $response['tag']['id'];
$payload = [
'name' => 'Form API test',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test',
'fields' => [
[
'label' => 'lab',
'alias' => 'email',
'type' => 'text',
'leadField' => 'email',
],
],
'actions' => [
[
'name' => 'Add tags to contact',
'description' => 'action description',
'type' => 'lead.changetags',
'order' => 1,
'properties' => [
'add_tags' => [$tag1Id],
'remove_tags' => [$tag2Id],
],
],
],
'postAction' => 'return',
];
// Create form with lead.changetags action:
$this->client->request('POST', '/api/forms/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']);
}
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, 'Return code must be 201.');
$formId = $response['form']['id'];
$this->assertGreaterThan(0, $formId);
$this->assertEquals($payload['name'], $response['form']['name']);
$this->assertEquals($payload['formType'], $response['form']['formType']);
$this->assertEquals($payload['isPublished'], $response['form']['isPublished']);
$this->assertEquals($payload['description'], $response['form']['description']);
$this->assertIsArray($response['form']['fields']);
$this->assertCount(count($payload['fields']), $response['form']['fields']);
for ($i = 0; $i < count($payload['fields']); ++$i) {
$this->assertEquals($payload['fields'][$i]['label'], $response['form']['fields'][$i]['label']);
$this->assertEquals($payload['fields'][$i]['alias'], $response['form']['fields'][$i]['alias']);
$this->assertEquals($payload['fields'][$i]['type'], $response['form']['fields'][$i]['type']);
$this->assertEquals($payload['fields'][$i]['leadField'], $response['form']['fields'][$i]['leadField']);
}
$this->assertIsArray($response['form']['actions']);
$this->assertCount(count($payload['actions']), $response['form']['actions']);
$this->assertEquals($payload['actions'][0]['name'], $response['form']['actions'][0]['name']);
$this->assertEquals($payload['actions'][0]['description'], $response['form']['actions'][0]['description']);
$this->assertEquals($payload['actions'][0]['type'], $response['form']['actions'][0]['type']);
$this->assertEquals($payload['actions'][0]['order'], $response['form']['actions'][0]['order']);
$this->assertIsArray($response['form']['actions'][0]['properties']['add_tags']);
$this->assertIsArray($response['form']['actions'][0]['properties']['remove_tags']);
$this->assertEquals($tag1Payload['tag'], $response['form']['actions'][0]['properties']['add_tags'][0]);
$this->assertEquals($tag2Payload['tag'], $response['form']['actions'][0]['properties']['remove_tags'][0]);
}
public function testFormWithDuplicateFieldAliases(): void
{
// Create form
$payload = [
'name' => 'Form API test',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test',
'fields' => [
[
'label' => 'Email',
'alias' => 'email',
'type' => 'text',
'leadField' => 'email',
],
],
'postAction' => 'return',
];
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$lastValidFormId = $response['form']['id'];
$this->assertGreaterThan(0, $lastValidFormId);
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, 'Return code must be 201.');
// Get the last correctly saved form
$this->client->request(Request::METHOD_GET, '/api/forms/'.$lastValidFormId);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertIsArray($response['form']);
$this->assertCount(1, $response);
$this->assertEquals($lastValidFormId, $response['form']['id']);
// Try to update invalid, non-existent form
$longAlias = 'very_long_field_alias_12345';
$invalidPayload = [
'name' => 'Form API test',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test',
'fields' => [
[
'label' => 'test1',
'alias' => 'very_long_field_alias_12345',
'type' => 'text',
],
[
'label' => 'test2',
'alias' => 'very_long_field_alias_123456',
'type' => 'text',
],
],
];
$this->client->request(Request::METHOD_PUT, '/api/forms/123/edit', $invalidPayload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$validationMessage = 'Another field is already using this alias: %alias%. Please choose another or leave it blank to have it autogenerated.';
$expectedMessage = str_replace('%alias%', substr($longAlias, 0, 25), $validationMessage);
$this->assertNotEmpty($response['errors'], 'No errors were returned when trying to save an invalid form');
$this->assertSame(Response::HTTP_BAD_REQUEST, $response['errors'][0]['code'], 'Return code must be 400.');
$this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST, 'Return code must be 400.');
$this->assertSame($expectedMessage, $response['errors'][0]['message'], 'Returned message is different than expected');
}
public function testFormWithInvalidField(): void
{
$payload = [
'name' => 'Form API test',
'formType' => 'standalone',
'isPublished' => true,
'description' => 'Functional API test',
'fields' => [
[
'label' => 'test1',
'alias' => 'test1',
'type' => 'text',
],
[
'label' => 'test2',
'id' => 123,
'alias' => 'test2',
'type' => 'invalidField',
],
],
];
$this->client->request(Request::METHOD_PUT, '/api/forms/123/edit', $payload);
$response = $this->client->getResponse();
$responseContent = json_decode($response->getContent());
$this->assertNotEmpty($responseContent->errors, 'No errors were returned when trying to save an invalid form');
$this->assertSame('Form Field ID 123 not found', $responseContent->errors[0]->message);
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND, 'Return code must be 404.');
}
}

View File

@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller\Api;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Form;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\HttpFoundation\Response;
final class FormApiControllerTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
/**
* @param array<string, mixed> $formData
*/
#[DataProvider('formDataProvider')]
public function testCreateFormWithFieldsAndActions(array $formData, int $expectedStatusCode): void
{
$this->client->request(
'POST',
'/api/forms/new',
$formData,
);
$response = $this->client->getResponse();
$this->assertSame($expectedStatusCode, $response->getStatusCode());
$this->assertResponseIsSuccessful();
$responseData = json_decode($response->getContent(), true);
$this->assertArrayHasKey('form', $responseData);
$this->assertArrayHasKey('id', $responseData['form']);
$this->assertArrayHasKey('name', $responseData['form']);
$this->assertSame($formData['name'], $responseData['form']['name']);
// Verify fields were created if provided
if (isset($formData['fields'])) {
$this->assertArrayHasKey('fields', $responseData['form']);
$this->assertCount(count($formData['fields']), $responseData['form']['fields']);
foreach ($formData['fields'] as $index => $expectedField) {
$actualField = $responseData['form']['fields'][$index];
$this->assertSame($expectedField['label'], $actualField['label']);
$this->assertSame($expectedField['type'], $actualField['type']);
if (isset($expectedField['alias'])) {
$this->assertSame($expectedField['alias'], $actualField['alias']);
}
}
}
// Verify actions were created if provided
if (isset($formData['actions'])) {
$this->assertArrayHasKey('actions', $responseData['form']);
$this->assertCount(count($formData['actions']), $responseData['form']['actions']);
}
}
/**
* @param array<string, mixed> $initialFormData
* @param array<string, mixed> $updateData
*/
#[DataProvider('updateFormDataProvider')]
public function testUpdateFormWithFieldsAndActions(array $initialFormData, array $updateData, int $expectedStatusCode): void
{
$form = $this->createForm($initialFormData);
$this->client->request(
'PUT',
'/api/forms/'.$form->getId().'/edit',
$updateData,
);
$response = $this->client->getResponse();
$this->assertSame($expectedStatusCode, $response->getStatusCode());
$this->assertResponseIsSuccessful();
$responseData = json_decode($response->getContent(), true);
$this->assertArrayHasKey('form', $responseData);
$this->assertSame($form->getId(), $responseData['form']['id']);
// Verify updates were applied
if (isset($updateData['name'])) {
$this->assertSame($updateData['name'], $responseData['form']['name']);
}
// Verify field updates/deletions for PUT requests
if (isset($updateData['fields'])) {
$this->assertArrayHasKey('fields', $responseData['form']);
$this->assertCount(count($updateData['fields']), $responseData['form']['fields']);
}
}
public function testCreateFormWithDuplicateFieldAliasesHandledCorrectly(): void
{
$formData = [
'name' => 'Test Form with Duplicate Aliases',
'fields' => [
[
'label' => 'First Field',
'type' => 'text',
'alias' => 'duplicate_alias',
],
[
'label' => 'Second Field',
'type' => 'text',
'alias' => 'duplicate_alias',
],
],
];
$this->client->request(
'POST',
'/api/forms/new',
$formData,
);
$response = $this->client->getResponse();
// The form creation should be rejected due to duplicate aliases
$this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
$responseData = json_decode($response->getContent(), true);
// Check for error response - could be either 'error' or 'errors' key depending on API format
$this->assertTrue(
isset($responseData['error']) || isset($responseData['errors']),
'Response should contain error information'
);
// Check that the error mentions the duplicate alias
if (isset($responseData['error']['message'])) {
$this->assertStringContainsString('duplicate_alias', $responseData['error']['message']);
} elseif (isset($responseData['errors'][0]['message'])) {
$this->assertStringContainsString('duplicate_alias', $responseData['errors'][0]['message']);
}
}
public function testCreateFormWithInvalidFieldData(): void
{
$formData = [
'name' => 'Test Form with Invalid Field',
'fields' => [
[
'label' => '', // Empty label should cause validation error
'type' => 'text',
],
],
];
$this->client->request(
'POST',
'/api/forms/new',
$formData,
);
$response = $this->client->getResponse();
$this->assertSame(Response::HTTP_BAD_REQUEST, $response->getStatusCode());
}
public function testUpdateFormRemovesUnspecifiedFieldsOnPut(): void
{
// Create form with multiple fields
$form = $this->createForm([
'name' => 'Test Form',
'fields' => [
['label' => 'Field 1', 'type' => 'text'],
['label' => 'Field 2', 'type' => 'email'],
['label' => 'Field 3', 'type' => 'textarea'],
],
]);
// Update with PUT - only include one field (should remove others)
$firstField = $form->getFields()->first();
$this->assertNotFalse($firstField, 'Form should have at least one field');
$updateData = [
'name' => 'Updated Form',
'fields' => [
[
'id' => $firstField->getId(),
'label' => 'Updated Field 1',
'type' => 'text',
],
],
];
$this->client->request(
'PUT',
'/api/forms/'.$form->getId().'/edit',
$updateData,
);
$response = $this->client->getResponse();
$this->assertResponseIsSuccessful();
$responseData = json_decode($response->getContent(), true);
$this->assertCount(1, $responseData['form']['fields']);
$this->assertSame('Updated Field 1', $responseData['form']['fields'][0]['label']);
}
/**
* @return array<string, array<mixed>>
*/
public static function formDataProvider(): array
{
return [
'simple form' => [
[
'name' => 'Simple Test Form',
'description' => 'A simple test form',
],
Response::HTTP_CREATED,
],
'form with fields' => [
[
'name' => 'Form with Fields',
'fields' => [
[
'label' => 'First Name',
'type' => 'text',
'alias' => 'first_name',
],
[
'label' => 'Email Address',
'type' => 'email',
'alias' => 'email',
],
],
],
Response::HTTP_CREATED,
],
];
}
/**
* @return array<string, array<mixed>>
*/
public static function updateFormDataProvider(): array
{
return [
'update name only' => [
['name' => 'Original Form'],
['name' => 'Updated Form Name'],
Response::HTTP_OK,
],
'add fields to existing form' => [
['name' => 'Form without fields'],
[
'name' => 'Form with fields',
'fields' => [
[
'label' => 'New Field',
'type' => 'text',
],
],
],
Response::HTTP_OK,
],
];
}
/**
* @param array<string, mixed> $data
*/
private function createForm(array $data): Form
{
$form = new Form();
$form->setName($data['name']);
$form->setDescription($data['description'] ?? '');
$form->setAlias($data['alias'] ?? strtolower(str_replace(' ', '_', $data['name'])));
$this->em->persist($form);
$this->em->flush();
// If fields are provided, create them through the API to properly test the preSaveEntity method
if (isset($data['fields'])) {
$this->client->request(
'PUT',
'/api/forms/'.$form->getId().'/edit',
[
'name' => $form->getName(),
'fields' => $data['fields'],
]
);
// Refresh the form entity to get the updated data
$this->em->refresh($form);
}
return $form;
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Symfony\Component\DomCrawler\Crawler;
final class AutoFillReadOnlyFormSubmissionTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
/**
* @param array<string, bool|null> $data
* @param array<string, string> $expected
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataForReadOnlyConfigurationSetting')]
public function testFieldConfiguration(array $data, array $expected): void
{
// Create a form
$form = $this->createForm();
$emailField = $this->createFormField($form, 'Email', 'email', $data['isAutoFill'], $data['isReadOnly'], 'email', 'contact');
$form->addField(1, $emailField);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$this->assertResponseIsSuccessful();
$formElement = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$this->client->submit($formElement);
$this->assertResponseIsSuccessful();
$this->client->xmlHttpRequest('GET', sprintf('/s/forms/field/edit/%d?formId=%d', $emailField->getId(), $form->getId()));
$this->assertResponseIsSuccessful();
$response = $this->client->getResponse();
$content = json_decode($response->getContent())->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formValues = $crawler->selectButton('Update')->form()->getPhpValues();
$this->assertSame($expected['isAutoFill'], $formValues['formfield']['isAutoFill']);
$this->assertSame($expected['isReadOnly'], $formValues['formfield']['isReadOnly']);
}
/**
* @return iterable<string, array<int, array<string, bool|string|null>>>
*/
public static function dataForReadOnlyConfigurationSetting(): iterable
{
yield 'When no behaviour configured' => [
// given
[
'isAutoFill' => null,
'isReadOnly' => null,
],
// expected
[
'isAutoFill' => '0',
'isReadOnly' => '0',
],
];
yield 'When field set to read only' => [
// given
[
'isAutoFill' => true,
'isReadOnly' => true,
],
// expected
[
'isAutoFill' => '1',
'isReadOnly' => '1',
],
];
yield 'When field set to read only and not autofill' => [
// given
[
'isAutoFill' => false,
'isReadOnly' => true,
],
// expected
[
'isAutoFill' => '0',
'isReadOnly' => '1',
],
];
yield 'When field set to autofill and not read only' => [
// given
[
'isAutoFill' => true,
'isReadOnly' => false,
],
// expected
[
'isAutoFill' => '1',
'isReadOnly' => '0',
],
];
}
public function testAutoFilledFormForReadOnlyAttribute(): void
{
$form = $this->createFormWithFields();
$formId = $form->getId();
// Initial request
$crawler = $this->client->request('GET', '/form/'.$formId);
$this->assertResponseIsSuccessful();
$this->assertInputCounts($crawler, 0);
$formValues = ['john@doe.com', 'John', 'Doe'];
// Submit the form
$formCrawler = $crawler->filter('form[id=mauticform_test]');
$form = $formCrawler->form([
'mauticform[email]' => $formValues[0],
'mauticform[firstname]' => $formValues[1],
'mauticform[lastname]' => $formValues[2],
]);
$this->client->submit($form);
// Request the form again
$crawler = $this->client->request('GET', '/form/'.$formId);
$this->assertResponseIsSuccessful();
$this->assertInputCounts($crawler, 2);
$readOnlyInput = $crawler->filterXPath('//input[@readonly]');
$readOnlyInput->each(function (Crawler $node, $i) use ($formValues) {
$this->assertStringContainsString('readonly', $node->outerHtml());
$this->assertSame($formValues[$i], $node->attr('value'));
});
}
private function assertInputCounts(Crawler $crawler, int $readonly): void
{
$this->assertCount(3, $crawler->filterXPath('//input[not(@type="hidden")]'));
$this->assertCount(6, $crawler->filterXPath('//input'));
$this->assertCount($readonly, $crawler->filterXPath('//input[@readonly]'));
}
private function createFormWithFields(): Form
{
$form = $this->createForm();
$emailField = $this->createFormField($form, 'Email', 'email', true, true, 'email', 'contact');
$form->addField(1, $emailField);
$firstNameField = $this->createFormField($form, 'First name', 'text', true, true, 'firstname', 'contact');
$form->addField(2, $firstNameField);
$lastNameField = $this->createFormField($form, 'Last name', 'text', false, true, 'lastname', 'contact');
$form->addField(3, $lastNameField);
$submitButton = $this->createFormField($form, 'Submit', 'button');
$form->addField(4, $submitButton);
$this->em->flush();
$this->em->clear();
return $form;
}
private function createForm(): Form
{
$form = new Form();
$form->setName('Test');
$form->setAlias('test');
$form->setPostActionProperty('Success');
$this->em->persist($form);
return $form;
}
private function createFormField(
Form $form,
string $label,
string $type,
?bool $isAutoFill = false,
?bool $isReadOnly = false,
?string $mappedField = null,
?string $mappedObject = null,
): Field {
$field = new Field();
$field->setLabel($label);
$field->setType($type);
$field->setForm($form);
$field->setAlias(strtolower(str_replace(' ', '', $label)));
$field->setIsAutoFill($isAutoFill);
$field->setIsReadOnly($isReadOnly);
$field->setMappedObject($mappedObject);
$field->setMappedField($mappedField);
$this->em->persist($field);
return $field;
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Form;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class FieldControllerFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testNewEmailFieldFormIsPreMapped(): void
{
$this->client->xmlHttpRequest(
Request::METHOD_GET,
'/s/forms/field/new?type=email&tmpl=field&formId=temporary_form_hash&inBuilder=1'
);
$clientResponse = $this->client->getResponse();
$payload = json_decode($clientResponse->getContent(), true);
self::assertResponseIsSuccessful();
Assert::assertStringContainsString('<option value="email" selected="selected">', $payload['newContent']);
}
public function testNewCaptchaFieldFormCanBeSaved(): void
{
$payload = [
'name' => 'Submission test form',
'description' => 'Form created via captcha test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'email',
'alias' => 'email',
'leadField' => 'email',
],
[
'label' => 'Submit',
'type' => 'button',
],
],
'postAction' => 'return',
];
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$formId = $response['form']['id'];
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED, $clientResponse->getContent());
$crawler = $this->client->xmlHttpRequest(Request::METHOD_GET, "/s/forms/field/new?type=captcha&tmpl=field&formId={$formId}&inBuilder=1");
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formCrawler = $crawler->filter('form[name=formfield]');
Assert::assertCount(1, $formCrawler, $this->client->getResponse()->getContent());
$form = $formCrawler->form();
$form->setValues(
[
'formfield[formId]' => $formId,
'formfield[type]' => 'captcha',
'formfield[label]' => 'What is the capital of Czech Republic?',
'formfield[properties][captcha]' => 'Prague',
]
);
$this->setCsrfHeader();
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles());
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
Assert::assertSame(1, $response['success'] ?? null, $this->client->getResponse()->getContent());
Assert::assertSame(1, $response['closeModal'] ?? null, $this->client->getResponse()->getContent());
}
public function testNewCompanyLookupFieldForm(): void
{
$form = new Form();
$form->setName('Test Form')
->setIsPublished(true)
->setAlias('testform');
$this->em->persist($form);
$this->em->flush();
$this->client->xmlHttpRequest(
Request::METHOD_GET,
'/s/forms/field/new?type=companyLookup&tmpl=field&formId='.$form->getId().'&inBuilder=1'
);
Assert::assertTrue($this->client->getResponse()->isOk());
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$this->assertSame('Contact', $crawler->filter('select[id="formfield_mappedObject"]')->filter('option[selected]')->text());
$this->assertSame('Primary company', $crawler->filter('select[id="formfield_mappedField"]')->filter('option[selected]')->text());
}
/**
* @param array<string, mixed>|null $additionalValues
*/
#[\PHPUnit\Framework\Attributes\DataProvider('provideFieldTypesData')]
public function testFieldWithLinkInLabel(
string $fieldType,
string $label,
string $expectedHtmlFragment,
string $helpMessage = '',
?array $additionalValues = null,
): void {
$this->client->xmlHttpRequest(
Request::METHOD_GET,
sprintf('/s/forms/field/new?type=%s&tmpl=field&formId=temporary_form_hash&inBuilder=1', $fieldType)
);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formCrawler = $crawler->filter('form[name=formfield]');
Assert::assertCount(1, $formCrawler, $this->client->getResponse()->getContent());
$form = $formCrawler->form();
$form->setValues(
[
'formfield[formId]' => 'temporary_form_hash',
'formfield[label]' => $label,
'formfield[helpMessage]' => $helpMessage,
]
);
$values = $form->getPhpValues();
if ($additionalValues) {
$values = array_merge_recursive($values, $additionalValues);
}
$this->setCsrfHeader();
$this->client->xmlHttpRequest($form->getMethod(), $form->getUri(), $values, $form->getPhpFiles());
$this->assertResponseIsSuccessful();
$response = json_decode($this->client->getResponse()->getContent(), true);
$this->assertStringContainsString($expectedHtmlFragment, $response['fieldHtml']);
}
/**
* @return array<string, array{
* fieldType: string,
* label: string,
* expectedHtmlFragment: string,
* additionalValues: array<string, mixed>|null
* }>
*/
public static function provideFieldTypesData(): array
{
return [
'email field with link in label' => [
'fieldType' => 'email',
'label' => 'Email <a href="https://example.com" target="_blank">link</a>',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => '',
'additionalValues' => null,
],
'email field with link in helpMessage' => [
'fieldType' => 'email',
'label' => 'Email',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => 'Find more info at <a href="https://example.com" target="_blank">link</a>',
'additionalValues' => null,
],
'checkbox group field with link in label' => [
'fieldType' => 'checkboxgrp',
'label' => 'Checkbox Group <a href="https://example.com" target="_blank">link</a>',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => '',
'additionalValues' => [
'formfield' => [
'properties' => [
'optionlist' => [
'list' => [
[
'label' => 'option1',
'value' => 'option1',
],
],
],
],
],
],
],
'checkbox group field with link in helpMessage' => [
'fieldType' => 'checkboxgrp',
'label' => 'Checkbox Group',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => 'Find <a href="https://example.com" target="_blank">link</a>',
'additionalValues' => [
'formfield' => [
'properties' => [
'optionlist' => [
'list' => [
[
'label' => 'option1',
'value' => 'option1',
],
],
],
],
],
],
],
'checkbox group field with link in option label' => [
'fieldType' => 'checkboxgrp',
'label' => 'Checkbox Group',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">terms and conditions</a>',
'helpMessage' => '',
'additionalValues' => [
'formfield' => [
'properties' => [
'optionlist' => [
'list' => [
[
'label' => 'I agree with the <a href="https://example.com" target="_blank">terms and conditions</a>.',
'value' => '1',
],
],
],
],
],
],
],
'select field with link in label' => [
'fieldType' => 'select',
'label' => 'Select',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => 'Get <a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'additionalValues' => [
'formfield' => [
'properties' => [
'list' => [
'list' => [
[
'label' => 'abc',
'value' => 'abc',
],
],
],
],
],
],
],
'select field with link in helpMessage' => [
'fieldType' => 'select',
'label' => 'Select <a href="https://example.com" target="_blank">link</a>',
'expectedHtmlFragment' => '<a href="https://example.com" target="_blank" rel="noreferrer noopener">link</a>',
'helpMessage' => '',
'additionalValues' => [
'formfield' => [
'properties' => [
'list' => [
'list' => [
[
'label' => 'abc',
'value' => 'abc',
],
],
],
],
],
],
],
];
}
}

View File

@@ -0,0 +1,786 @@
<?php
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Helper\LanguageHelper;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\EmailBundle\Entity\Email;
use Mautic\FormBundle\Entity\Action;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\ProjectBundle\Entity\Project;
use PHPUnit\Framework\Assert;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
class FormControllerFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
protected function setUp(): void
{
parent::setUp();
if ('testLabelsForFormAction' === $this->name()) {
$this->truncateTables('assets', 'categories', 'emails', 'lead_lists');
}
}
/**
* Index should return status code 200.
*/
public function testIndexActionWhenNotFiltered(): void
{
$this->client->request('GET', '/s/forms');
$this->assertTrue($this->client->getResponse()->isOk());
}
/**
* Filtering should return status code 200.
*/
public function testIndexActionWhenFiltering(): void
{
$this->client->request('GET', '/s/forms?search=has%3Aresults&tmpl=list');
$this->assertTrue($this->client->getResponse()->isOk());
}
/**
* Get form's create page.
*/
public function testNewActionForm(): void
{
$this->client->request('GET', '/s/forms/new/');
$this->assertTrue($this->client->getResponse()->isOk());
}
/**
* @see https://github.com/mautic/mautic/issues/10453
*/
public function testSaveActionForm(): void
{
$crawler = $this->client->request('GET', '/s/forms/new/');
$this->assertTrue($this->client->getResponse()->isOk());
$form = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$form->setValues(
[
'mauticform[name]' => 'Test',
'mauticform[renderStyle]' => '0',
]
);
$crawler = $this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk());
$form = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$form->setValues(
[
'mauticform[renderStyle]' => '0',
]
);
// The form failed to save when saved for the second time with renderStyle=No.
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
$this->assertStringNotContainsString('Internal Server Error - Expected argument of type "null or string", "boolean" given', $this->client->getResponse()->getContent());
}
public function testNewActionCheckDisplayMessageOptionsForm(): void
{
$this->client->request('GET', '/s/forms/new');
$this->assertTrue($this->client->getResponse()->isOk());
$clientResponse = $this->client->getResponse();
self::assertResponseStatusCodeSame(Response::HTTP_OK, $clientResponse->getContent());
$this->assertStringContainsString('Hide form', $clientResponse->getContent(), $clientResponse->getContent());
$this->assertStringContainsString('Redirect URL', $clientResponse->getContent(), $clientResponse->getContent());
$this->assertStringContainsString('Remain at form', $clientResponse->getContent(), $clientResponse->getContent());
}
public function testErrorValidationWithHideFormTypeWithoutMessage(): void
{
$crawler = $this->client->request('GET', '/s/forms/new/');
$this->assertTrue($this->client->getResponse()->isOk());
$selectedValue = $crawler->filter('#mauticform_postAction option:selected')->attr('value');
$this->assertEquals('message', $selectedValue);
$form = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$form->setValues(
[
'mauticform[name]' => 'Test',
'mauticform[postAction]' => 'hideform',
]
);
$crawler = $this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk());
$divClass = $crawler->filter('#mauticform_postActionProperty')->ancestors()->first()->attr('class');
$this->assertStringContainsString('has-error', $divClass, $crawler->html());
}
public function testSuccessWithHideForm(): void
{
$crawler = $this->client->request('GET', '/s/forms/new/');
$this->assertTrue($this->client->getResponse()->isOk());
$selectedValue = $crawler->filter('#mauticform_postAction option:selected')->attr('value');
$this->assertEquals('message', $selectedValue);
$form = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$form->setValues(
[
'mauticform[name]' => 'Test',
'mauticform[postAction]' => 'hideform',
'mauticform[postActionProperty]' => 'message',
]
);
$crawler = $this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
$divClass = $crawler->filter('#mauticform_postActionProperty')->ancestors()->first()->attr('class');
$this->assertStringNotContainsString('has-error', $divClass, $crawler->html());
}
public function testLanguageForm(): void
{
$translationsPath = __DIR__.'/resource/language/fr';
$languagePath = __DIR__.'/../../../../../translations/fr';
$filesystem = new Filesystem();
// copy all from $translationsPath to $languagePath
$filesystem->mirror($translationsPath, $languagePath);
/** @var LanguageHelper $languageHelper */
$languageHelper = $this->getContainer()->get('mautic.helper.language');
$formPayload = [
'name' => 'Test Form',
'formType' => 'campaign',
'language' => 'fr',
'postAction' => 'return',
'fields' => [
[
'label' => 'Email',
'alias' => 'email',
'type' => 'email',
'leadField' => 'email',
'isRequired' => true,
], [
'label' => 'Submit',
'alias' => 'submit',
'type' => 'button',
],
],
];
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), json_encode($languageHelper->getLanguageChoices()));
$form = $response['form'];
$formId = $form['id'];
$crawler = $this->client->request('GET', '/form/'.$form['id']);
$this->assertStringContainsString('Merci de patienter...', $crawler->html());
$this->assertStringContainsString('Ceci est requis.', $crawler->html());
$filesystem->remove($languagePath);
}
public function testMappedFieldIsNotMarkedAsRemappedUponSavingTheForm(): void
{
$form = $this->createForm('Test', 'test');
$field = $this->createFormField([
'label' => 'Email',
'type' => 'email',
])->setForm($form);
// @phpstan-ignore-next-line (using the deprecated method on purpose)
$field->setLeadField('email');
$this->em->persist($field);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$this->assertTrue($this->client->getResponse()->isOk());
$formElement = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$this->client->submit($formElement);
$response = $this->client->getResponse();
$this->assertTrue($response->isOk());
$this->assertStringNotContainsString('contact: Email', $response->getContent(), 'Email field should not be marked as mapped.');
}
public function testMappedFieldIsNotAutoFilledWhenUpdatingField(): void
{
$form = $this->createForm('Test', 'test');
$field = $this->createFormField([
'label' => 'Email',
'type' => 'email',
])->setForm($form);
$field->setMappedObject(null);
$field->setMappedField(null);
$this->em->persist($field);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$this->assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
$formElement = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$this->client->submit($formElement);
$this->assertTrue($this->client->getResponse()->isOk());
$this->client->xmlHttpRequest('GET', sprintf('/s/forms/field/edit/%d?formId=%d', $field->getId(), $form->getId()));
$response = $this->client->getResponse();
$this->assertTrue($response->isOk());
$this->assertJson($response->getContent());
$content = json_decode($response->getContent())->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$options = $crawler->filterXPath('//select[@name="formfield[mappedField]"]')->html();
$this->assertStringContainsString('<option value="email">Email</option>', $options, 'Email option should not be pre-selected.');
}
public function testMappedFieldCheckboxGroup(): void
{
// Create custom boolean field.
$customField = new LeadField();
$customField->setObject('lead');
$customField->setType('boolean');
$customField->setLabel('Custom Bool Field');
$customField->setAlias('custom_boolean_field');
$customField->setProperties([
'yes' => 'Absolutely yes',
'no' => 'Obviously No',
]);
// Create & add checkbox group type field to form.
$form = $this->createForm('Test form', 'test_form');
$field = $this->createFormField([
'label' => 'Test Checkbox Group',
'type' => 'checkboxgrp',
]);
$field->setMappedObject('contact');
$field->setMappedField('custom_boolean_field');
$fieldProperties = [
'list' => [
'option1' => 'First Option',
'option2' => 'Second Option',
],
];
$field->setProperties($fieldProperties);
$field->setForm($form);
$this->em->persist($field);
$this->em->flush();
$this->em->clear();
// Verify form creation
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$this->assertResponseIsSuccessful();
// Visit the form preview page
$crawler = $this->client->request('GET', sprintf('/s/forms/preview/%d', $form->getId()));
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('First Option', $this->client->getResponse()->getContent());
$this->assertStringContainsString('Second Option', $this->client->getResponse()->getContent());
}
public function testCreateNewActionUsingBaseTemplateToDisplay(): void
{
// Create new form
$form = $this->createForm('Test', 'test');
$this->em->persist($form);
// Fetch the form
$this->client->xmlHttpRequest(Request::METHOD_GET, '/s/forms/action/new',
[
'formId' => $form->getId(),
'type' => 'lead.addutmtags',
]
);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formCrawler = $crawler->filter('form');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
// Save new Send Form Results action
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$actionHtml = json_decode($content, true)['actionHtml'] ?? null;
$this->assertNotNull($actionHtml, $content);
$crawler = new Crawler($actionHtml);
$editPage = $crawler->filter('.btn-edit')->attr('href');
// Check the content was not changed
$this->client->xmlHttpRequest(Request::METHOD_GET, $editPage);
$this->assertResponseIsSuccessful();
}
public function testEditNewActionUsingBaseTemplateToDisplay(): void
{
// Create new form
$form = $this->createForm('Test', 'test');
// Create action
$action = $this->createFormAction($form, 'lead.addutmtags');
$form->addAction(0, $action);
$this->em->persist($form);
$this->em->flush();
$this->em->clear();
// Edit and submit the form to be able to push action into session
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$formElement = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$this->client->submit($formElement);
$this->assertResponseIsSuccessful();
// Update the Action
$this->setCsrfHeader();
$this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
$this->client->xmlHttpRequest(
Request::METHOD_POST,
sprintf('/s/forms/action/edit/%s?formId=%s', $action->getId(), $form->getId()),
['formId' => $form->getId()], // Query parameters (handled in URL)
[], // Files
['CONTENT_TYPE' => 'application/json'], // server
json_encode([
'formaction' => [
'id' => $action->getId(),
'name' => $action->getName(),
'type' => 'lead.addutmtags',
'order' => $action->getOrder(),
'properties' => [],
'formId' => $form->getId(),
],
])
);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$content = json_decode($content)->newContent;
$crawler = new Crawler($content, $this->client->getInternalRequest()->getUri());
$formCrawler = $crawler->filter('form');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
$this->client->submit($form);
$this->assertResponseIsSuccessful();
$content = $this->client->getResponse()->getContent();
$actionHtml = json_decode($content, true)['actionHtml'] ?? null;
$this->assertNotNull($actionHtml, $content);
$crawler = new Crawler($actionHtml);
$editPage = $crawler->filter('.btn-edit')->attr('href');
// Check the content was not changed
$this->client->xmlHttpRequest(Request::METHOD_GET, $editPage);
$this->assertResponseIsSuccessful();
}
/**
* @param array{
* type: string,
* properties: array<string, mixed>,
* entities?: array<object>
* } $inputValues The input configuration for the form action
* @param array<int, array{
* message: string,
* message_arg: array<string, mixed>
* }> $expectedMessages The expected messages with translation arguments
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataTestLabelsForFormActions')]
public function testLabelsForFormAction(array $inputValues, array $expectedMessages): void
{
$form = $this->createForm('test', 'test');
// Persist entities if provided
if (!empty($inputValues['entities'])) {
foreach ($inputValues['entities'] as $entity) {
$this->em->persist($entity);
}
}
// create form action
$action = $this->createFormAction($form, $inputValues['type'], $inputValues['properties']);
$form->addAction(0, $action);
$this->em->persist($form);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', sprintf('/s/forms/edit/%d', $form->getId()));
$this->assertResponseIsSuccessful();
$translator = $this->getContainer()->get('translator');
\assert($translator instanceof TranslatorInterface);
foreach ($expectedMessages as $expectedMessage) {
$translatedMessage = $translator->trans($expectedMessage['message'], $expectedMessage['message_arg']);
$this->assertStringContainsString(strip_tags($translatedMessage), $crawler->html());
}
}
/**
* @return iterable<string, array{
* 0: array{
* type: string,
* properties: array<string, mixed>,
* entities?: array<object>
* },
* 1: array<array{
* message: string,
* message_arg: array<string, mixed>
* }>
* }>
*/
public static function dataTestLabelsForFormActions(): iterable
{
$category = new Category();
$category->setTitle('Category');
$category->setAlias('category');
$category->setBundle('global');
$asset = new Asset();
$asset->setTitle('test');
$asset->setAlias('test');
$asset->setCategory($category);
yield 'Action: Download asset using category' => [
// input
[
'type' => 'asset.download',
'properties' => [
'asset' => null,
'category' => 1,
],
'entities' => [
$category,
$asset,
],
],
// expected
[
[
'message' => 'mautic.form.field.asset.use_category',
'message_arg' => [
'%category_name%' => $category->getTitle(),
],
],
],
];
yield 'Action: Add to company points' => [
// input
[
'type' => 'lead.scorecontactscompanies',
'properties' => ['score' => 10],
],
// expected
[
[
'message' => 'mautic.form.form.change_points_by',
'message_arg' => ['%value%' => 10],
],
],
];
yield 'Action: Add to contact points' => [
// input
[
'type' => 'lead.pointschange',
'properties' => [
'operator' => 'plus',
'points' => 10,
'group' => 0,
],
],
// expected
[
[
'message' => 'mautic.form.field.points.operation',
'message_arg' => [
'%operator%' => '(+)',
'%points%' => 10,
'%group%' => '',
],
],
],
];
yield 'Action: Email to send to user' => [
// input
[
'type' => 'email.send.user',
'properties' => [
'useremail' => ['email' => 1],
'user_id' => [1],
],
'entities' => [
(new Email())->setName('Email')
->setSubject('Test Subject')
->setIsPublished(true),
],
],
// expected
[
[
'message' => 'Email',
'message_arg' => [],
],
[
'message' => 'Email',
'message_arg' => [],
],
],
];
$segmentOne = new LeadList();
$segmentOne->setName('list one');
$segmentOne->setAlias('list_one');
$segmentOne->setPublicName('list_one');
$segmentOne->setFilters([]);
$segmentTwo = new LeadList();
$segmentTwo->setName('list two');
$segmentTwo->setAlias('list_two');
$segmentTwo->setPublicName('list_two');
$segmentTwo->setFilters([]);
yield 'Action: Change segments' => [
// input
[
'type' => 'lead.changelist',
'properties' => [
'addToLists' => [1],
'removeFromLists' => [2],
],
'entities' => [
$segmentOne,
$segmentTwo,
],
],
// expected
[
[
'message' => $segmentOne->getName(),
'message_arg' => [],
],
[
'message' => $segmentTwo->getName(),
'message_arg' => [],
],
],
];
}
/**
* @param array<string, int|string|array<mixed>> $properties
*/
private function createFormAction(Form $form, string $type, array $properties = []): Action
{
$action = new Action();
$action->setName($type);
$action->setType($type);
$action->setForm($form);
$action->setProperties($properties);
$this->em->persist($action);
return $action;
}
public function testCloneActionWithCondition(): void
{
$form = $this->createForm('Conditional Form', 'Conditional Form');
$this->em->flush();
$field1 = $this->createFormField([
'label' => 'Country',
'type' => 'country',
'mappedObject' => 'contact',
'mappedField' => 'country',
])->setForm($form);
$this->em->persist($field1);
$field2 = $this->createFormField([
'label' => 'State',
'mappedObject' => 'contact',
'mappedField' => 'state',
'conditions' => [
'any' => 0,
'expr' => 'in',
'values' => ['United States'],
],
'parent' => $field1->getId(),
])->setForm($form);
$fieldSubmit = $this->createFormField([
'label' => 'Submit',
'type' => 'button',
])->setForm($form);
$this->em->persist($field2);
$this->em->flush();
$form->addField($field1->getId(), $field1);
$form->addField($field2->getId(), $field2);
$form->addField($fieldSubmit->getId(), $fieldSubmit);
$field2->setParent((string) $field1->getId());
$this->em->persist($form);
$this->em->flush();
// request for form clone
$crawler = $this->client->request(Request::METHOD_GET, "/s/forms/clone/{$form->getId()}");
$mauticform = $crawler->filterXPath('//form[@name="mauticform"]')->form();
$mauticform['mauticform[name]']->setValue('Clone Conditional Form');
$mauticform['mauticform[isPublished]']->setValue('1');
$this->client->submit($mauticform);
$this->assertTrue($this->client->getResponse()->isOk());
$forms = $this->em->getRepository(Form::class)->findBy([], ['id' => 'ASC']);
Assert::assertCount(2, $forms);
$originalForm = $forms[0];
$clonedForm = $forms[1];
Assert::assertSame($form->getId(), $originalForm->getId());
Assert::assertNotSame($form->getId(), $clonedForm->getId());
$fields = $clonedForm->getFields()->getValues();
Assert::assertCount(3, $fields);
list($clonedField1, $clonedField2, $clonedSubmit) = $fields;
Assert::assertSame((int) $clonedField2->getParent(), $clonedField1->getId());
}
public function testFormWithProject(): void
{
$form = $this->createForm('Name', 'Alias');
$project = new Project();
$project->setName('Test Project');
$this->em->persist($project);
$this->em->flush();
$this->em->clear();
$crawler = $this->client->request('GET', '/s/forms/edit/'.$form->getId());
$formCrawler = $crawler->selectButton('Save')->form();
$formCrawler['mauticform[projects]']->setValue((string) $project->getId());
$this->client->submit($formCrawler);
$this->assertResponseIsSuccessful();
$savedForm = $this->em->find(Form::class, $form->getId());
Assert::assertSame($project->getId(), $savedForm->getProjects()->first()->getId());
}
public function testFormDetailsViewWithPreviewPanel(): void
{
// Create a form
$form = $this->createForm('Test Form Details', 'test_form_details');
$this->em->persist($form);
$this->em->flush();
// Request the form details view
$crawler = $this->client->request('GET', sprintf('/s/forms/view/%d', $form->getId()));
$this->assertResponseIsSuccessful();
// Check if preview panel exists
$previewPanel = $crawler->filter('div.panel.shd-none.bdr-rds-0.bdr-w-0.mt-sm.mb-0');
if ($previewPanel->count() > 0) {
// If preview panel exists, verify its structure
$panelHeading = $previewPanel->filter('.panel-heading .panel-title:contains("Preview")');
$this->assertCount(1, $panelHeading, 'Preview panel should have correct heading structure');
$panelBody = $previewPanel->filter('.panel-body.pt-xs');
$this->assertCount(1, $panelBody, 'Preview panel should have correct body structure');
}
}
public function testSliderFieldRendersWithInputAttributes(): void
{
// Create a form with a slider field
$form = $this->createForm('Test Slider Form', 'test_slider_form');
$this->em->persist($form);
$this->em->flush();
// Create a slider field
$sliderField = $this->createFormField([
'label' => 'Test Slider',
'type' => 'slider',
]);
$sliderField->setProperties([
'min' => 0,
'max' => 100,
'step' => 5,
]);
$sliderField->setForm($form);
$sliderField->setOrder(1);
$this->em->persist($sliderField);
$this->em->flush();
$this->em->clear();
// Request the form preview instead of view
$crawler = $this->client->request('GET', sprintf('/s/forms/preview/%d', $form->getId()));
$this->assertResponseIsSuccessful();
// Check that the slider input has the oninput attribute
$sliderInput = $crawler->filter('input[type="range"]');
$this->assertCount(1, $sliderInput, 'Slider input should be present');
$oninputAttr = $sliderInput->attr('oninput');
$this->assertNotNull($oninputAttr, 'Slider input should have oninput attribute');
$this->assertStringContainsString('document.getElementById', $oninputAttr, 'Slider input should use getElementById to target output element');
$this->assertStringContainsString('.textContent = this.value', $oninputAttr, 'Slider input should set output value to input value');
}
private function createForm(string $name, string $alias): Form
{
$form = new Form();
$form->setName($name);
$form->setAlias($alias);
$form->setPostActionProperty('Success');
$this->em->persist($form);
return $form;
}
/**
* @param array<string,mixed> $data
*/
private function createFormField(array $data = []): Field
{
$field = new Field();
$aliasSlug = strtolower(str_replace(' ', '_', $data['label'] ?? 'Field 1'));
$field->setLabel($data['label'] ?? 'Field 1');
$field->setAlias('field_'.$aliasSlug);
$field->setType($data['type'] ?? 'text');
$field->setMappedObject($data['mappedObject'] ?? '');
$field->setMappedField($data['mappedField'] ?? '');
$field->setConditions($data['conditions'] ?? []);
$this->em->persist($field);
return $field;
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\FormBundle\Entity\Form;
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
final class FormProjectSearchFunctionalTest 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');
$formAlpha = $this->createForm('Form Alpha');
$formBeta = $this->createForm('Form Beta');
$this->createForm('Form Gamma');
$this->createForm('Form Delta');
$formAlpha->addProject($projectOne);
$formAlpha->addProject($projectTwo);
$formBeta->addProject($projectTwo);
$formBeta->addProject($projectThree);
$this->em->flush();
$this->em->clear();
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/forms', '/s/forms']);
}
/**
* @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' => ['Form Alpha', 'Form Beta'],
'unexpectedEntities' => ['Form Gamma', 'Form Delta'],
];
yield 'search by one project AND form name' => [
'searchTerm' => 'project:"Project Two" AND Beta',
'expectedEntities' => ['Form Beta'],
'unexpectedEntities' => ['Form Alpha', 'Form Gamma', 'Form Delta'],
];
yield 'search by one project OR form name' => [
'searchTerm' => 'project:"Project Two" OR Gamma',
'expectedEntities' => ['Form Alpha', 'Form Beta', 'Form Gamma'],
'unexpectedEntities' => ['Form Delta'],
];
yield 'search by NOT one project' => [
'searchTerm' => '!project:"Project Two"',
'expectedEntities' => ['Form Gamma', 'Form Delta'],
'unexpectedEntities' => ['Form Alpha', 'Form Beta'],
];
yield 'search by two projects with AND' => [
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
'expectedEntities' => ['Form Beta'],
'unexpectedEntities' => ['Form Alpha', 'Form Gamma', 'Form Delta'],
];
yield 'search by two projects with NOT AND' => [
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
'expectedEntities' => ['Form Gamma', 'Form Delta'],
'unexpectedEntities' => ['Form Alpha', 'Form Beta'],
];
yield 'search by two projects with OR' => [
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
'expectedEntities' => ['Form Alpha', 'Form Beta'],
'unexpectedEntities' => ['Form Gamma', 'Form Delta'],
];
yield 'search by two projects with NOT OR' => [
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
'expectedEntities' => ['Form Alpha', 'Form Gamma', 'Form Delta'],
'unexpectedEntities' => ['Form Beta'],
];
}
private function createForm(string $name): Form
{
$form = new Form();
$form->setName($name);
$form->setAlias($name);
$this->em->persist($form);
return $form;
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\LeadBundle\Entity\Company;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class PublicControllerFunctionalTest extends MauticMysqlTestCase
{
public function testLookupActionWithNoLookupFormField(): void
{
$this->makeRequest(['string' => 'Company']);
$clientResponse = $this->client->getResponse();
self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST, $clientResponse->getContent());
Assert::assertSame('{"error":"Invalid request param"}', $clientResponse->getContent(), $clientResponse->getContent());
}
public function testLookupActionWithInvalidLookupFormField(): void
{
$this->makeRequest(['string' => 'Company', 'formId' => 3]);
$clientResponse = $this->client->getResponse();
self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST, $clientResponse->getContent());
Assert::assertSame('{"error":"Invalid request param"}', $clientResponse->getContent(), $clientResponse->getContent());
}
public function testLookupActionWithTooFewLetters(): void
{
$form = $this->createForm();
$this->makeRequest(['string' => 'Co', 'formId' => $form->getId()]);
$clientResponse = $this->client->getResponse();
self::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST, $clientResponse->getContent());
Assert::assertSame('{"error":"Invalid request param"}', $clientResponse->getContent(), $clientResponse->getContent());
}
public function testLookupActionWithCompanyData(): void
{
$this->createCompany('Unicorn A');
$companyA = $this->createCompany('Company A');
$companyB = $this->createCompany('Company B', 'Boston', 'Massachusetts');
$form = $this->createForm();
$this->makeRequest(['search' => 'Company', 'formId' => $form->getId()]);
$clientResponse = $this->client->getResponse();
Assert::assertTrue($clientResponse->isOk(), $clientResponse->getContent());
Assert::assertSame(
[
[
'id' => (string) $companyA->getId(),
'companyname' => 'Company A',
'companycity' => null,
'companystate' => null,
], [
'id' => (string) $companyB->getId(),
'companyname' => 'Company B',
'companycity' => 'Boston',
'companystate' => 'Massachusetts',
],
],
json_decode($clientResponse->getContent(), true)
);
}
/**
* @param mixed[] $payload
*/
private function makeRequest(array $payload): void
{
$this->client->request(
Request::METHOD_POST,
'/form/company-lookup/autocomplete',
[],
[],
['Content-Type' => 'application/json'],
json_encode($payload)
);
}
private function createCompany(string $name, ?string $city = null, ?string $state = null): Company
{
$company = new Company();
$company->setName($name);
$company->setCity($city);
$company->setState($state);
$this->em->persist($company);
$this->em->flush();
return $company;
}
private function createForm(): Form
{
$field = new Field();
$field->setAlias('company-lookup');
$field->setLabel('Company');
$field->setType('companyLookup');
$form = new Form();
$form->setName('Company Lookup Test');
$form->setAlias('company-lookup-test');
$form->addField(0, $field);
$field->setForm($form);
$this->em->persist($field);
$this->em->persist($form);
$this->em->flush();
return $form;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class ResultControllerFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testDownloadFileByFileNameAction(): void
{
$fieldModel = static::getContainer()->get('mautic.form.model.field');
$formUploader = static::getContainer()->get('mautic.form.helper.form_uploader');
$fileName = 'image.png';
$this->createFile($fileName);
$formPayload = [
'name' => 'API form',
'formType' => 'standalone',
'alias' => 'apiform',
'description' => 'Test API Form',
'isPublished' => true,
'fields' => [
[
'label' => 'File',
'alias' => 'file_field',
'type' => 'file',
'properties' => [
'allowed_file_size' => 1,
'allowed_file_extensions' => ['txt', 'jpg', 'gif', 'png'],
'public' => true,
],
],
],
'postAction' => 'return',
];
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$form = $response['form'];
$formId = $form['id'];
$fieldId = $form['fields'][0]['id'];
$crawler = $this->client->request(Request::METHOD_GET, "/form/{$formId}");
$formCrawler = $crawler->filter('form[id=mauticform_apiform]');
$form = $formCrawler->form();
$file = new UploadedFile($fileName, $fileName, 'image/png');
$form->setValues([
'mauticform[file_field]' => $file,
]);
$this->client->submit($form);
$this->assertTrue($this->client->getResponse()->isOk());
$this->client->request(Request::METHOD_GET, "/forms/results/file/{$fieldId}/filename/{$fileName}");
$this->assertTrue($this->client->getResponse()->isOk());
$field = $fieldModel->getEntity($fieldId);
unlink($fileName);
unlink($formUploader->getCompleteFilePath($field, $fileName));
$folderPath = str_replace(DIRECTORY_SEPARATOR.$fileName, '', $formUploader->getCompleteFilePath($field, $fileName));
if (is_dir($folderPath)) {
rmdir($folderPath);
}
}
public function testAddToSegmentActionRendersBatchForm(): void
{
// Create a form
$formPayload = [
'name' => 'Segment Test Form',
'formType' => 'standalone',
'alias' => 'segmenttestform',
'description' => 'Form for segment batch test',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'alias' => 'email',
'type' => 'email',
'properties' => [],
],
],
'postAction' => 'return',
];
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$form = $response['form'];
$formId = $form['id'];
// Submit a form result (simulate a contact submission)
$this->client->request('POST', "/form/{$formId}", [
'mauticform[email]' => 'test@example.com',
'mauticform[formId]' => $formId,
'mauticform[return]' => '',
]);
$this->assertResponseIsSuccessful();
// Call the addToSegmentAction
$this->client->request('GET', "/s/forms/results/{$formId}/add-to-segment");
$response = $this->client->getResponse();
$this->assertResponseIsSuccessful();
$this->assertStringContainsString('form', $response->getContent());
$this->assertStringContainsString('batch', $response->getContent());
}
public function testEditButtonIsDisplayedOnFormResultsPage(): void
{
$formPayload = [
'name' => 'Test Form for Results',
'formType' => 'standalone',
'alias' => 'testformresults',
'description' => 'Test Form for Results Page',
'isPublished' => true,
'fields' => [
[
'label' => 'Name',
'alias' => 'name',
'type' => 'text',
],
],
'postAction' => 'return',
];
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$form = $response['form'];
$formId = $form['id'];
$crawler = $this->client->request(Request::METHOD_GET, "/s/forms/results/{$formId}");
$response = $this->client->getResponse();
if (!$response->isOk()) {
$this->fail('Response is not OK. Status: '.$response->getStatusCode().', Content: '.$response->getContent());
}
$editButton = $crawler->filter('a[href*="/s/forms/edit/'.$formId.'"]');
$this->assertCount(1, $editButton, 'Edit button should be present on form results page');
}
private function createFile(string $filename): void
{
$data = 'data:image/png;base64,AAAFBfj42Pj4';
[$type, $data] = explode(';', $data);
[, $data] = explode(',', $data);
$data = base64_decode($data);
file_put_contents($filename, $data);
}
}

View File

@@ -0,0 +1 @@
mautic.form.submission.pleasewait="Merci de patienter..."

View File

@@ -0,0 +1 @@
mautic.form.field.generic.required="Ceci est requis."

View File

@@ -0,0 +1 @@
{"name":"French","locale":"fr","author":"Mautic Translators"}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Crate;
use Mautic\FormBundle\Crate\FieldCrate;
use PHPUnit\Framework\Assert;
final class FieldCrateTest extends \PHPUnit\Framework\TestCase
{
public function testGettersForEmailField(): void
{
$field = new FieldCrate('6', 'Email', 'email', []);
Assert::assertSame('6', $field->getKey());
Assert::assertSame('Email', $field->getName());
Assert::assertSame('email', $field->getType());
Assert::assertSame([], $field->getProperties());
Assert::assertFalse($field->isListType());
}
public function testGettersForSelectField(): void
{
$properties = [
'list' => [
'Red' => 'red',
'Green' => 'green',
],
];
$field = new FieldCrate('7', 'Colors', 'select', $properties);
Assert::assertSame('7', $field->getKey());
Assert::assertSame('Colors', $field->getName());
Assert::assertSame('select', $field->getType());
Assert::assertSame($properties, $field->getProperties());
Assert::assertTrue($field->isListType());
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Crate;
use Mautic\FormBundle\Crate\ObjectCrate;
use PHPUnit\Framework\Assert;
final class ObjectCrateTest extends \PHPUnit\Framework\TestCase
{
public function testGetters(): void
{
$field = new ObjectCrate('contact', 'Contact');
Assert::assertSame('contact', $field->getKey());
Assert::assertSame('Contact', $field->getName());
}
}

View File

@@ -0,0 +1,342 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Entity;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\LeadBundle\Entity\Lead;
use PHPUnit\Framework\Assert;
final class FieldTest extends \PHPUnit\Framework\TestCase
{
public function testShowForConditionalFieldWithNoParent(): void
{
$field = new Field();
$this->assertTrue($field->showForConditionalField([]));
}
public function testShowForConditionalFieldWithParentButNoAlias(): void
{
$parentFieldId = '55';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$parentField->method('getId')->willReturn($parentFieldId);
$this->assertFalse($field->showForConditionalField([]));
}
public function testShowForConditionalFieldWithParentAndAliasAndNotInConditionAndBadValue(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'notIn', 'values' => []]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => 'value A'];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasWith0ValueAndNotInConditionAndBadValue(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'notIn', 'values' => [1]]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => 0];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndNotInConditionAndMatchingValue(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'notIn', 'values' => ['value A']]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => 'value A'];
$this->assertFalse($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndAnyValue(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => '', 'any' => true, 'values' => ['value A']]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => 'value A'];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentValue0AndAliasAndAnyValue(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => '', 'any' => true, 'values' => [1]]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => 0];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndInValueMatches(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'in', 'values' => ['value A']]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => ['value A']];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndInValueDoesNotMatch(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'in', 'values' => ['value B']]);
$parentField->method('getId')->willReturn(55);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => ['value A']];
$this->assertFalse($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndInValueMatchesWithDifferentTypes(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$field->setConditions(['expr' => 'in', 'values' => ['0']]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => [0]];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForConditionalFieldWithParentAndAliasAndInValueMatchesSpecialCharacters(): void
{
$parentFieldId = '55';
$parentFieldAlias = 'field_a';
$field = new Field();
$parentField = $this->createMock(Field::class);
$form = new Form();
$form->addField(0, $parentField);
$field->setForm($form);
$field->setParent($parentFieldId);
$specialValue = 'čé+äà>&"\'è';
$field->setConditions(['expr' => 'in', 'values' => [InputHelper::clean($specialValue)]]);
$parentField->method('getId')->willReturn($parentFieldId);
$parentField->method('getAlias')->willReturn($parentFieldAlias);
$data = [$parentFieldAlias => [$specialValue]];
$this->assertTrue($field->showForConditionalField($data));
}
public function testShowForContactIfFormIsNull(): void
{
$field = new Field();
Assert::assertTrue($field->showForContact());
}
public function testShowForContactIfInKioskMode(): void
{
$field = new Field();
$form = new Form();
$form->setInKioskMode(true);
Assert::assertTrue($field->showForContact(null, null, $form));
}
public function testShowForContactIfShowWhenValueExistsIsTrue(): void
{
$field = new Field();
$form = new Form();
$form->setInKioskMode(false);
$field->setShowWhenValueExists(true);
Assert::assertTrue($field->showForContact(null, null, $form));
}
public function testShowForContactIfShowWhenValueExistsIsFalseAndSubmissionExists(): void
{
$field = new Field();
$form = new Form();
$submissions = [['field_a' => 'Value A']];
$form->setInKioskMode(false);
$field->setShowWhenValueExists(false);
$field->setIsAutoFill(false);
$field->setAlias('field_a');
Assert::assertFalse($field->showForContact($submissions, null, $form));
}
public function testShowForContactIfShowWhenValueExistsIsFalseAndSubmissionDoesNotExist(): void
{
$field = new Field();
$form = new Form();
$submissions = [['field_a' => 'Value A']];
$form->setInKioskMode(false);
$field->setShowWhenValueExists(false);
$field->setIsAutoFill(false);
$field->setAlias('unicorn');
Assert::assertTrue($field->showForContact($submissions, null, $form));
}
public function testShowForContactIfShowWhenValueExistsIsFalseAndMappedLeadFieldValueExists(): void
{
$field = new Field();
$form = new Form();
$contact = new class extends Lead {
public function getFieldValue($field, $group = null)
{
Assert::assertSame('field_a', $field);
return 'Value A';
}
};
$form->setInKioskMode(false);
$field->setShowWhenValueExists(false);
$field->setMappedField('field_a');
$field->setMappedObject('contact');
$field->setIsAutoFill(false);
Assert::assertFalse($field->showForContact(null, $contact, $form));
}
public function testShowForContactIfShowWhenValueExistsIsFalseAndMappedLeadFieldValueDoesNotExist(): void
{
$field = new Field();
$form = new Form();
$contact = new class extends Lead {
public function getFieldValue($field, $group = null)
{
Assert::assertSame('field_a', $field);
return null;
}
};
$form->setInKioskMode(false);
$field->setShowWhenValueExists(false);
$field->setMappedField('field_a');
$field->setMappedObject('contact');
$field->setIsAutoFill(false);
Assert::assertTrue($field->showForContact(null, $contact, $form));
}
public function testShowForContactIfShowWhenValueExistsIsFalseAndMappedNotLeadFieldValueExists(): void
{
$field = new Field();
$form = new Form();
$contact = new class extends Lead {
public function getFieldValue($field, $group = null)
{
Assert::assertSame('field_a', $field);
return 'Value A';
}
};
$form->setInKioskMode(false);
$field->setShowWhenValueExists(false);
$field->setMappedField('field_a');
$field->setMappedObject('unicorn_object');
$field->setIsAutoFill(false);
Assert::assertTrue($field->showForContact(null, $contact, $form));
}
/**
* @param array<string, int> $properties
*/
#[\PHPUnit\Framework\Attributes\DataProvider('dataProvider')]
public function testHasChoices(string $type, array $properties, bool $result): void
{
$field = new Field();
$field->setProperties($properties);
$field->setType($type);
$this->assertEquals($result, $field->hasChoices());
}
/**
* @return array<int, mixed>
*/
public static function dataProvider(): iterable
{
yield ['string', [], false];
yield ['string', ['multiple' => 0], false];
yield ['string', ['multiple' => 1], true];
yield ['checkboxgrp', [], true];
yield ['checkboxgrp', ['multiple' => 0], true];
yield ['checkboxgrp', ['multiple' => 1], true];
}
public function testFieldWidth(): void
{
$field = new Field();
$this->assertEquals('100%', $field->getFieldWidth(), 'Default field width should be 100%');
$field->setFieldWidth('50%');
$this->assertEquals('50%', $field->getFieldWidth(), 'Field width should be updated to 50%');
$field->setFieldWidth('');
$this->assertEquals('100%', $field->getFieldWidth(), 'Field width should default to 100% when set to empty string');
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Entity;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use PHPUnit\Framework\Assert;
final class FormTest extends \PHPUnit\Framework\TestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('setNoIndexDataProvider')]
public function testSetNoIndex($value, $expected, array $changes): void
{
$form = new Form();
$form->setNoIndex($value);
Assert::assertSame($expected, $form->getNoIndex());
Assert::assertSame($changes, $form->getChanges());
}
public static function setNoIndexDataProvider(): iterable
{
yield [null, null, ['noIndex' => [true, null]]];
yield [true, true, []];
yield [false, false, ['noIndex' => [true, false]]];
yield ['', false, ['noIndex' => [true, false]]];
yield [0, false, ['noIndex' => [true, false]]];
yield ['string', true, []];
}
public function testGetMappedFieldValues(): void
{
$form = $this->createForm();
$result = [
[
'formFieldId' => null,
'mappedObject' => 'contact',
'mappedField' => 'email',
],
[
'formFieldId' => null,
'mappedObject' => 'company',
'mappedField' => 'companyemail',
],
[
'formFieldId' => null,
'mappedObject' => 'company',
'mappedField' => 'companyname',
],
];
Assert::assertSame($result, $form->getMappedFieldValues());
}
public function testGetMappedFieldObjects(): void
{
$form = $this->createForm();
Assert::assertSame(['contact', 'company'], $form->getMappedFieldObjects());
}
private function createForm(): Form
{
$form = new Form();
$contactField = new Field();
$companyFieldA = new Field();
$companyFieldB = new Field();
$notMappedField = new Field();
$contactField->setMappedObject('contact');
$contactField->setMappedField('email');
$companyFieldA->setMappedObject('company');
$companyFieldA->setMappedField('companyemail');
$companyFieldB->setMappedObject('company');
$companyFieldB->setMappedField('companyname');
$form->addField('contact_field_a', $contactField);
$form->addField('company_field_a', $companyFieldA);
$form->addField('company_field_b', $companyFieldB);
$form->addField('not_mapped_field_a', $notMappedField);
return $form;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Event;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Event\FormFieldEvent;
final class FormFieldEventTest extends \PHPUnit\Framework\TestCase
{
public function testWorkflow(): void
{
$field = new Field();
$field2 = new Field();
$event = new FormFieldEvent($field, true);
$this->assertTrue($event->isNew());
$this->assertSame($field, $event->getField());
$event->setField($field2);
$this->assertSame($field2, $event->getField());
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Event\Service;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\Submission;
use Mautic\FormBundle\Event\Service\FieldValueTransformer;
use Mautic\FormBundle\Event\SubmissionEvent;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
final class FieldValueTransformerTest extends \PHPUnit\Framework\TestCase
{
public function testTransformValuesAfterSubmitWithNoFieldsNoMatchesAndNoTokens(): void
{
$router = new class extends Router {
public function __construct()
{
}
};
$transformer = new FieldValueTransformer($router);
$submission = new Submission();
$form = new Form();
$request = new Request();
$submissionEvent = new SubmissionEvent($submission, [], [], $request);
$submission->setForm($form);
$transformer->transformValuesAfterSubmit($submissionEvent);
Assert::assertSame([], $submissionEvent->getTokens());
Assert::assertSame([], $submissionEvent->getContactFieldMatches());
}
public function testTransformValuesAfterSubmitWithFileFieldMatchesAndTokens(): void
{
$router = new class extends Router {
public int $generateMethodCallCounter = 0;
public function __construct()
{
}
public function generate(string $name, mixed $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
Assert::assertSame('mautic_form_file_download', $name);
Assert::assertSame([
'submissionId' => 456,
'field' => 'file_field_1',
], $parameters);
Assert::assertSame(self::ABSOLUTE_URL, $referenceType);
++$this->generateMethodCallCounter;
return 'generated/route';
}
};
$transformer = new FieldValueTransformer($router);
$submission = new class extends Submission {
public function getId(): int
{
return 456;
}
};
$form = new Form();
$field = new Field();
$request = new Request();
$submissionEvent = new SubmissionEvent($submission, [], [], $request);
$field->setType('file');
$field->setAlias('file_field_1');
$field->setMappedField('contact_field_1');
$field->setMappedObject('contact');
$form->addField('123', $field);
$submission->setForm($form);
$submissionEvent->setTokens(['{formfield=file_field_1}' => 'original/route']);
$submissionEvent->setContactFieldMatches(['contact_field_1' => 'original/route']);
$transformer->transformValuesAfterSubmit($submissionEvent);
Assert::assertSame(['{formfield=file_field_1}' => 'generated/route'], $submissionEvent->getTokens());
Assert::assertSame(['{formfield=file_field_1}' => 'generated/route'], $transformer->getTokensToUpdate());
Assert::assertSame(['contact_field_1' => 'generated/route'], $submissionEvent->getContactFieldMatches());
Assert::assertSame(['contact_field_1' => 'generated/route'], $transformer->getContactFieldsToUpdate());
// Calling it for the second time to ensure it's executed only once.
$transformer->transformValuesAfterSubmit($submissionEvent);
Assert::assertSame(1, $router->generateMethodCallCounter);
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\EventListener;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\Event;
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\FormEvents;
use Mautic\LeadBundle\Entity\Lead;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class CampaignSubscriberFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
#[\PHPUnit\Framework\Attributes\DataProvider('valueProvider')]
public function testComparingFormSubmissionValues(string $valueToCompare, string $submittedValue, bool $result, string $operator = '='): void
{
$formPayload = [
'name' => 'Test Form',
'formType' => 'campaign',
'fields' => [
[
'label' => 'Select A',
'alias' => 'select_a',
'type' => 'select',
'properties' => [
'list' => [
'list' => [
[
'label' => $valueToCompare,
'value' => $valueToCompare,
],
[
'label' => $submittedValue,
'value' => $submittedValue,
],
],
],
'multiple' => false,
],
], [
'label' => 'Email',
'alias' => 'email',
'type' => 'email',
'leadField' => 'email',
], [
'label' => 'Submit',
'alias' => 'submit',
'type' => 'button',
],
],
'postAction' => 'return',
];
// Creating the form via API so it would create the submission table.
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
$formId = $response['form']['id'];
// Submitting the form.
$crawler = $this->client->request(Request::METHOD_GET, "/form/{$formId}");
$saveButton = $crawler->selectButton('mauticform[submit]');
$form = $saveButton->form();
$form['mauticform[select_a]']->setValue($submittedValue);
$form['mauticform[email]']->setValue('testing@ampersand.select');
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
// Create some necessary entities.
$campaignEvent = new Event();
$campaignEvent->setName('Test Event');
$campaignEvent->setType('form.field_value');
$campaignEvent->setEventType('condition');
$campaignEvent->setProperties(
[
'form' => $formId,
'field' => 'select_a',
'value' => $valueToCompare,
'operator' => $operator,
]
);
$campaign = new Campaign();
$campaign->setName('Test Campaign');
$campaign->addEvent(1, $campaignEvent);
$campaignEvent->setCampaign($campaign);
$this->em->persist($campaignEvent);
$this->em->persist($campaign);
$this->em->flush();
$this->em->detach($campaignEvent);
$this->em->detach($campaign);
$contact = $this->em->getRepository(Lead::class)->findOneBy(['email' => 'testing@ampersand.select']);
$event = new CampaignExecutionEvent(
[
'lead' => $contact,
'event' => $campaignEvent,
'eventDetails' => null,
'systemTriggered' => false,
'eventSettings' => [],
],
null
);
/** @var EventDispatcherInterface $dispatcher */
$dispatcher = static::getContainer()->get('event_dispatcher');
$dispatcher->dispatch($event, FormEvents::ON_CAMPAIGN_TRIGGER_CONDITION);
Assert::assertSame('form', $event->getChannel());
Assert::assertSame($result, $event->getResult());
}
public static function valueProvider(): \Generator
{
yield [
'test & test',
'test & test',
true,
];
yield [
'test & test',
'unicorn',
false,
];
yield [
'test',
'tester',
false,
'!like',
];
yield [
'test',
'tst',
true,
'!like',
];
}
protected function beforeTearDown(): void
{
$tablePrefix = static::getContainer()->getParameter('mautic.db_table_prefix');
if ($this->connection->createSchemaManager()->tablesExist("{$tablePrefix}form_results_1_test_form")) {
$this->connection->executeStatement("DROP TABLE {$tablePrefix}form_results_1_test_form");
}
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\EventListener;
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
use Mautic\FormBundle\Entity\SubmissionRepository;
use Mautic\FormBundle\EventListener\DetermineWinnerSubscriber;
use Mautic\PageBundle\Entity\Page;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Contracts\Translation\TranslatorInterface;
class DetermineWinnerSubscriberTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|SubmissionRepository
*/
private MockObject $submissionRepository;
/**
* @var MockObject|TranslatorInterface
*/
private MockObject $translator;
private DetermineWinnerSubscriber $subscriber;
protected function setUp(): void
{
parent::setUp();
$this->submissionRepository = $this->createMock(SubmissionRepository::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->subscriber = new DetermineWinnerSubscriber($this->submissionRepository, $this->translator);
}
public function testOnDetermineSubmissionWinner(): void
{
$parentMock = $this->createMock(Page::class);
$childMock = $this->createMock(Page::class);
$children = [2 => $childMock];
$parameters = ['parent' => $parentMock, 'children' => $children];
$event = new DetermineWinnerEvent($parameters);
$startDate = new \DateTime();
$transSubmissions = 'submissions';
$transHits = 'hits';
$counts = [
1 => [
'count' => 20,
'id' => 1,
'name' => 'Test 5',
'total' => 100,
],
2 => [
'count' => 25,
'id' => 2,
'name' => 'Test 6',
'total' => 150,
],
];
$this->translator->method('trans')
->willReturnOnConsecutiveCalls($transSubmissions, $transHits);
$parentMock->expects($this->any())
->method('isPublished')
->willReturn(true);
$childMock->expects($this->any())
->method('isPublished')
->willReturn(true);
$parentMock->expects($this->any())
->method('getId')
->willReturn(1);
$childMock->expects($this->any())
->method('getId')
->willReturn(2);
$parentMock->expects($this->once())
->method('getVariantStartDate')
->willReturn($startDate);
$this->submissionRepository->expects($this->once())
->method('getSubmissionCountsByPage')
->with([1, 2], $startDate)
->willReturn($counts);
$this->subscriber->onDetermineSubmissionWinner($event);
$expectedData = [
$transSubmissions => [$counts[1]['count'], $counts[2]['count']],
$transHits => [$counts[1]['total'], $counts[2]['total']],
];
$abTestResults = $event->getAbTestResults();
$this->assertEquals($abTestResults['winners'], [1]);
$this->assertEquals($abTestResults['support']['data'], $expectedData);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\EventListener;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Event\FormEvent;
use Mautic\FormBundle\EventListener\FormConditionalSubscriber;
use Mautic\FormBundle\Model\FieldModel;
use Mautic\FormBundle\Model\FormModel;
use PHPUnit\Framework\MockObject\MockObject;
final class FormConditionalSubscriberTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|FormModel
*/
private MockObject $formModel;
/**
* @var MockObject|FieldModel
*/
private MockObject $fieldModel;
private FormConditionalSubscriber $subscriber;
public function setUp(): void
{
parent::setUp();
$this->formModel = $this->createMock(FormModel::class);
$this->fieldModel = $this->createMock(FieldModel::class);
$this->subscriber = new FormConditionalSubscriber(
$this->formModel,
$this->fieldModel
);
}
public function testOnFormPostSaveForNewForm(): void
{
$parentField = $this->createMock(Field::class);
$childField = $this->createMock(Field::class);
$parentId = 'new_parent_id';
$childId = 'new_child_id';
$form = new Form();
$parentField->method('getId')->willReturn($parentId);
$parentField->method('getSessionId')->willReturn($parentId);
$childField->method('getId')->willReturn($childId);
$childField->method('getSessionId')->willReturn($childId);
$childField->method('getParent')->willReturn($parentId);
$form->addField(0, $parentField);
$form->addField(1, $childField);
$this->fieldModel->expects($this->once())
->method('saveEntity')
->with($parentField);
$this->formModel->expects($this->never())
->method('deleteFields');
$this->subscriber->onFormPostSave(new FormEvent($form, true));
}
/**
* A child field should be deleted when the parent does not exist anymore.
*/
public function testOnFormPostSaveForDeletedParent(): void
{
$childField = $this->createMock(Field::class);
$parentId = 123;
$childId = 456;
$form = new Form();
$childField->method('getId')->willReturn($childId);
$childField->method('getSessionId')->willReturn($childId);
$childField->method('getParent')->willReturn($parentId);
$form->addField(1, $childField);
$this->fieldModel->expects($this->never())
->method('saveEntity');
$this->formModel->expects($this->once())
->method('deleteFields')
->with($form, [$childId]);
$this->subscriber->onFormPostSave(new FormEvent($form, true));
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\EventListener;
use Mautic\FormBundle\EventListener\FormFieldSubscriber;
use Mautic\FormBundle\FormEvents;
use Mautic\FormBundle\Model\FieldModel;
use PHPUnit\Framework\TestCase;
final class FormFieldSubscriberTest extends TestCase
{
/**
* @var FormFieldSubscriber
*/
private $subscriber;
public function setUp(): void
{
parent::setUp();
$fieldModel = $this->createMock(FieldModel::class);
$this->subscriber = new FormFieldSubscriber($fieldModel);
}
public function testGetSubscribedEvents(): void
{
$this->assertEquals(
[
FormEvents::FIELD_POST_DELETE => ['onFieldPostDelete', 0],
],
$this->subscriber::getSubscribedEvents()
);
}
}

View File

@@ -0,0 +1,541 @@
<?php
namespace FormBundle\Tests\EventListener;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\LanguageHelper;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\FormBundle\Entity\Action;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\Submission;
use Mautic\FormBundle\Event\SubmissionEvent;
use Mautic\FormBundle\EventListener\FormSubscriber;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\UserBundle\Entity\User;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class FormSubscriberTest extends TestCase
{
private FormSubscriber $subscriber;
/**
* @var MailHelper&MockObject
*/
private MailHelper $mailer;
protected function setUp(): void
{
parent::setUp();
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
$auditLogModel = $this->createMock(AuditLogModel::class);
$this->mailer = $this->createMock(MailHelper::class);
$translator = $this->createMock(TranslatorInterface::class);
$router = $this->createMock(RouterInterface::class);
$languageHelper = $this->createMock(LanguageHelper::class);
$this->mailer->expects($this->once())
->method('getMailer')
->willReturnSelf();
$this->subscriber = new FormSubscriber(
$ipLookupHelper,
$auditLogModel,
$this->mailer,
$translator,
$router,
$languageHelper
);
}
public function testOnFormSubmitActionRepost(): void
{
$postData = [
'first_name' => "Test's Name un être> and être",
'notes' => 'A & B < dy >
New line',
'formId' => '1',
'return' => '',
'formName' => 'form190122',
'messenger' => '1',
];
$resultData = [
'first_name' => 'Test&#39;s Name un &ecirc;tre&gt; and être',
'notes' => 'A &#38; B &#60; dy &#62;&#10;New line',
];
$request = new Request();
$submission = $this->getFormSubmission();
$submissionEvent = new SubmissionEvent($submission, $postData, $request->server, $request);
$submissionEvent->setResults($resultData)
->setFields($this->getFormFields())
->setAction($this->getFormRepostAction());
$this->subscriber->onFormSubmitActionRepost($submissionEvent);
$postPayload = $submissionEvent->getPostSubmitPayload();
$this->assertSame([
$postData['first_name'],
$postData['notes'],
], [
$postPayload['first_name'],
$postPayload['notes'],
], 'Form data should be decode before posting to next form');
}
public function testOnFormSubmitSendsNothingIfNoEmailsWereSet(): void
{
$tokensData = [];
$request = new Request();
$submission = $this->getFormSubmission();
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => null,
'cc' => null,
'bcc' => null,
'copy_lead' => false,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::never())
->method('send');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
#[\PHPUnit\Framework\Attributes\DataProvider('toCcBccProvider')]
public function testOnFormSubmitSendsIfOneOfEmailsEmailsWereSet(?string $to, ?string $cc, ?string $bcc): void
{
$subject = 'subject';
$message = 'message';
$tokensData = [
'first_name' => 'Test&#39;s Name',
'notes' => "A &#38; B \n &#60; dy &#62;",
];
$emailTokens = [
'first_name' => "Test's Name",
'notes' => "A & B <br />\n < dy >",
];
$request = new Request();
$submission = $this->getFormSubmission();
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => $to,
'cc' => $cc,
'bcc' => $bcc,
'copy_lead' => false,
'subject' => $subject,
'message' => $message,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::once())
->method('reset');
$this->mailer->expects(self::once())
->method('send');
if (null !== $to) {
$this->mailer->expects(self::once())
->method('setTo')
->with(array_fill_keys(array_map('trim', explode(',', $to)), null));
}
if (null !== $cc) {
$this->mailer->expects(self::once())
->method('setCc')
->with(array_fill_keys(array_map('trim', explode(',', $cc)), null));
}
if (null !== $bcc) {
$this->mailer->expects(self::once())
->method('setBcc')
->with(array_fill_keys(array_map('trim', explode(',', $bcc)), null));
}
$this->mailer->expects(self::once())
->method('setSubject')
->with($subject);
$this->mailer->expects(self::once())
->method('setBody')
->with($message);
$this->mailer->expects(self::once())
->method('parsePlainText')
->with($message);
$this->mailer->expects(self::once())
->method('addTokens')
->with($emailTokens);
$this->mailer->expects(self::once())
->method('setLead');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
public static function toCcBccProvider(): \Generator
{
yield ['to@email.email, to2@email.email', null, null];
yield [null, 'cc@email.email, cc2@email.email', null];
yield [null, null, 'bcc@email.email, bcc2@email.email'];
}
public function testOnFormSubmitSendsIfCcAndBccWereSet(): void
{
$cc = 'cc@email.email, cc2@email.email';
$bcc = 'bcc@email.email, bcc@email.email'; // same emails will produce single email address
$subject = 'subject';
$message = 'message';
$tokensData = [
'first_name' => 'Test&#39;s Name',
'notes' => "A &#38; B \n &#60; dy &#62;",
];
$emailTokens = [
'first_name' => "Test's Name",
'notes' => "A & B <br />\n < dy >",
];
$request = new Request();
$submission = $this->getFormSubmission();
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => null,
'cc' => $cc,
'bcc' => $bcc,
'copy_lead' => false,
'email_to_owner' => false,
'subject' => $subject,
'message' => $message,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::once())
->method('reset');
$this->mailer->expects(self::once())
->method('send');
$this->mailer->expects(self::never())
->method('setTo');
$this->mailer->expects(self::once())
->method('setCc')
->with(array_fill_keys(array_map('trim', explode(',', $cc)), null));
$this->mailer->expects(self::once())
->method('setBcc')
->with(array_fill_keys(array_map('trim', explode(',', $bcc)), null));
$this->mailer->expects(self::once())
->method('setSubject')
->with($subject);
$this->mailer->expects(self::once())
->method('setBody')
->with($message);
$this->mailer->expects(self::once())
->method('parsePlainText')
->with($message);
$this->mailer->expects(self::once())
->method('addTokens')
->with($emailTokens);
$this->mailer->expects(self::once())
->method('setLead');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
public function testOnFormSubmitSendsIfCopyLeadEmailWasSet(): void
{
$subject = 'subject';
$message = 'message';
$leadEmail = 'lead@email.email';
$tokensData = [
'first_name' => 'Test&#39;s Name',
'notes' => "A &#38; B \n &#60; dy &#62;",
];
$emailTokens = [
'first_name' => "Test's Name",
'notes' => "A & B <br />\n < dy >",
];
$request = new Request();
$submission = $this->getFormSubmission();
$submission->getLead()->setEmail($leadEmail);
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => null,
'cc' => null,
'bcc' => null,
'copy_lead' => true,
'subject' => $subject,
'message' => $message,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::once())
->method('reset');
$this->mailer->expects(self::once())
->method('send');
$this->mailer->expects(self::once())
->method('setTo')
->with([$leadEmail => null]);
$this->mailer->expects(self::once())
->method('setSubject')
->with($subject);
$this->mailer->expects(self::once())
->method('setBody')
->with($message);
$this->mailer->expects(self::once())
->method('parsePlainText')
->with($message);
$this->mailer->expects(self::once())
->method('addTokens')
->with($emailTokens);
$this->mailer->expects(self::once())
->method('setLead');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
public function testOnFormSubmitSendsIfEmailToOwnerEmailWasSet(): void
{
$subject = 'subject';
$message = 'message';
$ownerEmail = 'lead@email.email';
$tokensData = [
'first_name' => 'Test&#39;s Name',
'notes' => "A &#38; B \n &#60; dy &#62;",
];
$emailTokens = [
'first_name' => "Test's Name",
'notes' => "A & B <br />\n < dy >",
];
$request = new Request();
$submission = $this->getFormSubmission();
$owner = new User();
$owner->setEmail($ownerEmail);
$submission->getLead()->setOwner($owner);
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => null,
'cc' => null,
'bcc' => null,
'copy_lead' => false,
'email_to_owner' => true,
'subject' => $subject,
'message' => $message,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::once())
->method('reset');
$this->mailer->expects(self::once())
->method('send');
$this->mailer->expects(self::once())
->method('setTo')
->with([$ownerEmail => null]);
$this->mailer->expects(self::once())
->method('setSubject')
->with($subject);
$this->mailer->expects(self::once())
->method('setBody')
->with($message);
$this->mailer->expects(self::once())
->method('parsePlainText')
->with($message);
$this->mailer->expects(self::once())
->method('addTokens')
->with($emailTokens);
$this->mailer->expects(self::once())
->method('setLead');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
public function testOnFormSubmitSendsIfAllWereSet(): void
{
$to = 'to@email.email';
$cc = 'cc@email.email, cc2@email.email';
$bcc = 'bcc@email.email';
$leadEmail = 'lead@email.email';
$ownerEmail = 'owner@email.email';
$subject = 'subject';
$message = 'message';
$tokensData = [
'first_name' => 'Test&#39;s Name',
'notes' => "A &#38; B \n &#60; dy &#62;",
];
$emailTokens = [
'first_name' => "Test's Name",
'notes' => "A & B <br />\n < dy >",
];
$request = new Request();
$submission = $this->getFormSubmission();
$owner = new User();
$owner->setEmail($ownerEmail);
$submission->getLead()->setOwner($owner);
$submission->getLead()->setEmail($leadEmail);
$submissionEvent = new SubmissionEvent($submission, [], $request->server, $request);
$action = $this->getFormSubmitActionSendEmail();
$action->setProperties([
'to' => $to,
'cc' => $cc,
'bcc' => $bcc,
'copy_lead' => true,
'email_to_owner' => true,
'subject' => $subject,
'message' => $message,
]);
$submissionEvent->setTokens($tokensData)
->setFields($this->getFormFields())
->setAction($action);
$this->mailer->expects(self::exactly(3))
->method('reset');
$this->mailer->expects(self::exactly(3))
->method('send');
$matcher = self::exactly(3);
$this->mailer->expects($matcher)
->method('setTo')->willReturnCallback(function (...$parameters) use ($matcher, $to, $leadEmail, $ownerEmail) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame([$to => null], $parameters[0]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame([$leadEmail => null], $parameters[0]);
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame([$ownerEmail => null], $parameters[0]);
}
return true;
});
$this->mailer->expects(self::once())
->method('setCc')
->with(array_fill_keys(array_map('trim', explode(',', $cc)), null));
$this->mailer->expects(self::once())
->method('setBcc')
->with([$bcc => null]);
$this->mailer->expects(self::exactly(3))
->method('setSubject')
->with($subject);
$this->mailer->expects(self::exactly(3))
->method('setBody')
->with($message);
$this->mailer->expects(self::exactly(3))
->method('parsePlainText')
->with($message);
$this->mailer->expects(self::exactly(3))
->method('addTokens')
->with($emailTokens);
$this->mailer->expects(self::exactly(3))
->method('setLead');
$this->subscriber->onFormSubmitActionSendEmail($submissionEvent);
}
/**
* @return string[][]
*/
private function getFormFields(): array
{
return [
1 => [
'id' => '1',
'type' => 'text',
'alias' => 'first_name',
],
2 => [
'id' => '2',
'type' => 'text',
'alias' => 'notes',
],
3 => [
'id' => '3',
'type' => 'button',
'alias' => 'submit',
],
];
}
private function getForm(): Form
{
$form = new Form();
$form->setName('Test Form');
$form->setAlias('test_form');
$form->setFormType('standalone');
return $form;
}
private function getFormRepostAction(): Action
{
$onSubmitActionConfig = [
'post_url' => 'https://mautic.org',
'failure_email' => '',
'authorization_header' => '',
];
$action = new Action();
return $action->setForm($this->getForm())
->setType('form.repost')
->setName('Test Action')
->setProperties($onSubmitActionConfig);
}
private function getFormSubmitActionSendEmail(): Action
{
$action = new Action();
return $action->setForm($this->getForm())
->setType('form.email')
->setName('Test Action');
}
private function getFormSubmission(): Submission
{
$lead = new Lead();
$lead->setFields($this->getFormFields());
$ipAddress = new IpAddress();
$ipAddress->setIpAddress('127.0.0.1');
$submission = new Submission();
return $submission->setForm($this->getForm())
->setLead($lead)
->setIpAddress($ipAddress);
}
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\EventListener;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\ReportBundle\Tests\Functional\AbstractReportSubscriberTestCase;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ReportSubscriberFunctionalTest extends AbstractReportSubscriberTestCase
{
public function testLeadReportWithDncListColumn(): void
{
$leads[] = $this->createContact('test1@example.com');
$leads[] = $this->createContact('test2@example.com');
$leads[] = $this->createContact('test3@example.com');
$this->em->flush();
$this->createDnc('email', $leads[0], DoNotContact::BOUNCED);
$this->createDnc('email', $leads[1], DoNotContact::MANUAL);
$this->createDnc('email', $leads[2], DoNotContact::UNSUBSCRIBED);
$this->createDnc('sms', $leads[2], DoNotContact::MANUAL);
$this->em->flush();
$formId = $this->createFormThroughApi();
$this->submitForm($formId, [
'email' => 'test1@example.com',
'firstname' => 'test1',
]);
$this->submitForm($formId, [
'email' => 'test2@example.com',
'firstname' => 'test2',
]);
$report = $this->createReport(
source: 'form.submissions',
columns: [
'l.id',
'l.firstname',
'dnc_preferences',
],
filters: [
[
'column' => 'f.id',
'glue' => 'and',
'dynamic' => null,
'condition' => 'eq',
'value' => $formId,
],
],
order: [['column' => 'l.id', 'direction' => 'ASC']]
);
$expectedReport = [
// id, firstname, dnc_preferences
[(string) $leads[0]->getId(), 'test1', 'DNC Bounced: Email'],
[(string) $leads[1]->getId(), 'test2', 'DNC Manually Unsubscribed: Email'],
];
$this->verifyReport($report->getId(), $expectedReport);
$this->verifyApiReport($report->getId(), $expectedReport);
}
public function testLeadReportWithDncListFilterIn(): void
{
$leads[] = $this->createContact('test1@example.com');
$leads[] = $this->createContact('test2@example.com');
$leads[] = $this->createContact('test3@example.com');
$this->em->flush();
$this->createDnc('email', $leads[0], DoNotContact::BOUNCED);
$this->createDnc('email', $leads[1], DoNotContact::MANUAL);
$this->createDnc('email', $leads[2], DoNotContact::UNSUBSCRIBED);
$this->createDnc('sms', $leads[2], DoNotContact::MANUAL);
$this->em->flush();
$formId = $this->createFormThroughApi();
$this->submitForm($formId, [
'email' => 'test1@example.com',
'firstname' => 'test1',
]);
$this->submitForm($formId, [
'email' => 'test2@example.com',
'firstname' => 'test2',
]);
$this->submitForm($formId, [
'email' => 'test3@example.com',
'firstname' => 'test3',
]);
$report = $this->createReport(
source: 'form.submissions',
columns: [
'l.id',
'l.firstname',
'dnc_preferences',
],
filters: [
[
'column' => 'f.id',
'glue' => 'and',
'dynamic' => null,
'condition' => 'eq',
'value' => $formId,
],
[
'column' => 'dnc_preferences',
'glue' => 'and',
'dynamic' => null,
'condition' => 'in',
'value' => [
'email:'.DoNotContact::UNSUBSCRIBED,
'email:'.DoNotContact::BOUNCED,
],
],
],
order: [['column' => 'l.id', 'direction' => 'ASC']]
);
$expectedReport = [
// id, firstname, dnc_preferences
[(string) $leads[0]->getId(), 'test1', 'DNC Bounced: Email'],
[(string) $leads[2]->getId(), 'test3', 'DNC Manually Unsubscribed: Text Message, DNC Unsubscribed: Email'],
];
$this->verifyReport($report->getId(), $expectedReport);
$this->verifyApiReport($report->getId(), $expectedReport);
}
private function createFormThroughApi(): int
{
$formPayload = [
'name' => 'Submission test form',
'description' => 'Form created via submission test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'email',
'alias' => 'email',
'leadField' => 'email',
'mappedField' => 'email',
'mappedObject' => 'contact',
],
[
'label' => 'Firstname',
'type' => 'text',
'alias' => 'firstname',
'leadField' => 'firstname',
'mappedField' => 'firstname',
'mappedObject' => 'contact',
],
[
'label' => 'Submit',
'type' => 'button',
],
],
'postAction' => 'return',
];
// Create the form
$this->client->request(Request::METHOD_POST, '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$formId = (int) $response['form']['id'];
$this->assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
return $formId;
}
/**
* @param array<string, mixed> $submissionData
*/
private function submitForm(int $formId, array $submissionData): Crawler
{
// Submit the form
$crawler = $this->client->request(Request::METHOD_GET, "/form/{$formId}");
$formCrawler = $crawler->filter('form[id=mauticform_submissiontestform]');
$this->assertCount(1, $formCrawler);
$form = $formCrawler->form();
$formData = [];
foreach ($submissionData as $key => $value) {
$formData["mauticform[{$key}]"] = $value;
}
$form->setValues($formData);
return $this->client->submit($form);
}
public function createDnc(string $channel, Lead $contact, int $reason): DoNotContact
{
$dnc = new DoNotContact();
$dnc->setChannel($channel);
$dnc->setLead($contact);
$dnc->setReason($reason);
$dnc->setDateAdded(new \DateTime());
$this->em->persist($dnc);
return $dnc;
}
private function createContact(string $email): Lead
{
$lead = new Lead();
$lead->setEmail($email);
$this->em->persist($lead);
return $lead;
}
}

View File

@@ -0,0 +1,396 @@
<?php
namespace Mautic\FormBundle\Tests\EventListener;
use Doctrine\DBAL\Query\QueryBuilder;
use Mautic\ChannelBundle\Helper\ChannelListHelper;
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Test\AbstractMauticTestCase;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\FormRepository;
use Mautic\FormBundle\Entity\SubmissionRepository;
use Mautic\FormBundle\EventListener\ReportSubscriber;
use Mautic\FormBundle\Model\FormModel;
use Mautic\LeadBundle\Model\CompanyReportData;
use Mautic\LeadBundle\Report\DncReportService;
use Mautic\ReportBundle\Event\ReportBuilderEvent;
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
use Mautic\ReportBundle\Event\ReportGraphEvent;
use Mautic\ReportBundle\Helper\ReportHelper;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Contracts\Translation\TranslatorInterface;
class ReportSubscriberTest extends AbstractMauticTestCase
{
/**
* @var CompanyReportData|MockObject
*/
private MockObject $companyReportData;
/**
* @var SubmissionRepository|MockObject
*/
private MockObject $submissionRepository;
/**
* @var FormModel|MockObject
*/
private MockObject $formModel;
/**
* @var FormRepository|MockObject
*/
private MockObject $formRepository;
private ReportHelper $reportHelper;
/**
* @var CoreParametersHelper|MockObject
*/
private MockObject $coreParametersHelper;
/**
* @var TranslatorInterface|MockObject
*/
private MockObject $translator;
private ReportSubscriber $subscriber;
private MockObject&DncReportService $dncReportService;
public function setUp(): void
{
$this->configParams['form_results_data_sources'] = true;
parent::setUp();
$this->companyReportData = $this->createMock(CompanyReportData::class);
$this->submissionRepository = $this->createMock(SubmissionRepository::class);
$this->formModel = $this->createMock(FormModel::class);
$this->formRepository = $this->createMock(FormRepository::class);
$this->reportHelper = new ReportHelper($this->createMock(EventDispatcher::class));
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->translator = $this->createMock(TranslatorInterface::class);
$this->dncReportService = $this->createMock(DncReportService::class);
$this->subscriber = new ReportSubscriber(
$this->companyReportData,
$this->submissionRepository,
$this->formModel,
$this->reportHelper,
$this->coreParametersHelper,
$this->translator,
$this->dncReportService
);
}
public function testOnReportBuilderAddsFormAndFormSubmissionReports(): void
{
$mockEvent = $this->getMockBuilder(ReportBuilderEvent::class)
->disableOriginalConstructor()
->onlyMethods([
'checkContext',
'addGraph',
'getStandardColumns',
'getCategoryColumns',
'getCampaignByChannelColumns',
'getLeadColumns',
'getIpColumn',
'addTable',
])
->getMock();
$mockEvent->expects($this->once())
->method('getStandardColumns')
->willReturn([]);
$mockEvent->expects($this->once())
->method('getCategoryColumns')
->willReturn([]);
$mockEvent->expects($this->once())
->method('getCampaignByChannelColumns')
->willReturn([]);
$mockEvent->expects($this->once())
->method('getLeadColumns')
->willReturn([]);
$mockEvent->expects($this->once())
->method('getIpColumn')
->willReturn([]);
$mockEvent->expects($this->exactly(3))
->method('checkContext')
->willReturnOnConsecutiveCalls(true, true, false);
$setTables = [];
$setGraphs = [];
$mockEvent->expects($this->exactly(2))
->method('addTable')
->willReturnCallback(function () use (&$setTables): void {
$args = func_get_args();
$setTables[] = $args;
});
$mockEvent->expects($this->exactly(3))
->method('addGraph')
->willReturnCallback(function () use (&$setGraphs): void {
$args = func_get_args();
$setGraphs[] = $args;
});
$this->companyReportData->expects($this->once())
->method('getCompanyData')
->with()
->willReturn([]);
$this->subscriber->onReportBuilder($mockEvent);
$this->assertCount(2, $setTables);
$this->assertCount(3, $setGraphs);
}
public function testOnReportBuilderWithWrongContext(): void
{
$reportBuilderEvent = new ReportBuilderEvent(
$this->translator,
$this->createMock(ChannelListHelper::class),
'test',
[],
$this->reportHelper,
''
);
$this->subscriber->onReportBuilder($reportBuilderEvent);
Assert::assertCount(0, $reportBuilderEvent->getTables());
}
public function testOnReportBuilderAddsFormAndFormResultReports(): void
{
$reportBuilderEvent = new ReportBuilderEvent(
$this->translator,
$this->createMock(ChannelListHelper::class),
ReportSubscriber::CONTEXT_FORM_RESULT,
[],
$this->reportHelper,
''
);
$field = new Field();
$field->setAlias('email');
$field->setType('string');
$field->setLabel('Email');
$field->setMappedObject('contact');
$field->setMappedField('email');
$form = new Form();
$form->addField('email', $field);
$field->setForm($form);
$this->formModel->expects($this->once())
->method('getRepository')
->willReturn($this->formRepository);
$this->formModel->expects($this->once())
->method('getCustomComponents')
->willReturn(['viewOnlyFields' => ['button', 'captcha', 'freetext', 'freehtml', 'pagebreak', 'plugin.loginSocial']]);
$this->formRepository->expects($this->once())
->method('getEntities')
->willReturn([[$form, 1]]);
$this->formRepository->expects($this->once())
->method('getResultsTableName')
->willReturn('test');
$this->subscriber->onReportBuilder($reportBuilderEvent);
$tables = $reportBuilderEvent->getTables();
Assert::assertCount(2, $tables);
Assert::assertArrayHasKey('form.results.test', $tables);
Assert::assertCount(3, $tables['form.results.test']['columns']);
}
public function testOnReportGenerateFormsContext(): void
{
$mockQueryBuilder = $this->createMock(QueryBuilder::class);
$mockEvent = $this->getMockBuilder(ReportGeneratorEvent::class)
->disableOriginalConstructor()
->onlyMethods([
'getContext',
'getQueryBuilder',
'addCategoryLeftJoin',
'setQueryBuilder',
])
->getMock();
$mockQueryBuilder->expects($this->once())
->method('from')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getQueryBuilder')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getContext')
->willReturn('forms');
$this->subscriber->onReportGenerate($mockEvent);
}
public function testOnReportGenerateFormSubmissionContext(): void
{
$mockQueryBuilder = $this->createMock(QueryBuilder::class);
$mockEvent = $this->getMockBuilder(ReportGeneratorEvent::class)
->disableOriginalConstructor()
->onlyMethods([
'getContext',
'getQueryBuilder',
'addCategoryLeftJoin',
'addIpAddressLeftJoin',
'addLeadLeftJoin',
'addCampaignByChannelJoin',
'applyDateFilters',
'setQueryBuilder',
])
->getMock();
$mockQueryBuilder->expects($this->once())
->method('from')
->willReturn($mockQueryBuilder);
$mockQueryBuilder->expects($this->exactly(2))
->method('leftJoin')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getQueryBuilder')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getContext')
->willReturn('form.submissions');
$this->subscriber->onReportGenerate($mockEvent);
}
public function testOnReportGenerateFormResultsContext(): void
{
$mockQueryBuilder = $this->createMock(QueryBuilder::class);
$mockEvent = $this->getMockBuilder(ReportGeneratorEvent::class)
->disableOriginalConstructor()
->onlyMethods([
'getContext',
'getQueryBuilder',
'addLeadLeftJoin',
'setQueryBuilder',
])
->getMock();
$mockQueryBuilder->expects($this->once())
->method('from')
->willReturn($mockQueryBuilder);
$mockQueryBuilder->expects($this->once())
->method('leftJoin')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getQueryBuilder')
->willReturn($mockQueryBuilder);
$mockEvent->expects($this->once())
->method('getContext')
->willReturn('form.results');
$this->subscriber->onReportGenerate($mockEvent);
}
public function testOnReportGraphGenerateBadContextWillReturn(): void
{
$mockEvent = $this->createMock(ReportGraphEvent::class);
$mockEvent->expects($this->once())
->method('checkContext')
->willReturn(false);
$mockEvent->expects($this->never())
->method('getRequestedGraphs');
$this->subscriber->onReportGraphGenerate($mockEvent);
}
public function testOnReportGraphGenerate(): void
{
$mockEvent = $this->createMock(ReportGraphEvent::class);
$mockTrans = $this->createMock(Translator::class);
$mockQueryBuilder = $this->createMock(QueryBuilder::class);
$mockChartQuery = $this->createMock(ChartQuery::class);
$mockTrans->expects($this->any())
->method('trans')
->willReturnArgument(0);
$mockEvent->expects($this->once())
->method('getQueryBuilder')
->willReturn($mockQueryBuilder);
$mockChartQuery->expects($this->any())
->method('loadAndBuildTimeData')
->willReturn(['a', 'b', 'c']);
$mockChartQuery->expects($this->any())
->method('fetchCount')
->willReturn(2);
$mockChartQuery->expects($this->any())
->method('fetchCountDateDiff')
->willReturn(2);
$graphOptions = [
'chartQuery' => $mockChartQuery,
'translator' => $mockTrans,
'dateFrom' => new \DateTime(),
'dateTo' => new \DateTime(),
];
$mockEvent->expects($this->once())
->method('checkContext')
->willReturn(true);
$mockEvent->expects($this->any())
->method('getOptions')
->willReturn($graphOptions);
$mockEvent->expects($this->once())
->method('getRequestedGraphs')
->willReturn(
[
'mautic.form.graph.line.submissions',
'mautic.form.table.top.referrers',
'mautic.form.table.most.submitted',
]
);
$this->submissionRepository->expects($this->once())
->method('getTopReferrers')
->willReturn(['a', 'b', 'c']);
$this->submissionRepository->expects($this->once())
->method('getMostSubmitted')
->willReturn(['a', 'b', 'c']);
$this->subscriber->onReportGraphGenerate($mockEvent);
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Form\Type;
use Mautic\FormBundle\Collection\FieldCollection;
use Mautic\FormBundle\Collection\ObjectCollection;
use Mautic\FormBundle\Collector\AlreadyMappedFieldCollectorInterface;
use Mautic\FormBundle\Collector\FieldCollectorInterface;
use Mautic\FormBundle\Collector\ObjectCollectorInterface;
use Mautic\FormBundle\Crate\FieldCrate;
use Mautic\FormBundle\Crate\ObjectCrate;
use Mautic\FormBundle\Form\Type\FieldType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
class FieldTypeTest extends TypeTestCase
{
private TranslatorInterface $translator;
private ObjectCollectorInterface $objectCollector;
private FieldCollectorInterface $fieldCollector;
private AlreadyMappedFieldCollectorInterface $mappedFieldCollector;
protected function setUp(): void
{
$this->translator = $this->createMock(TranslatorInterface::class);
$this->objectCollector = $this->createMock(ObjectCollectorInterface::class);
$this->fieldCollector = $this->createMock(FieldCollectorInterface::class);
$this->mappedFieldCollector = $this->createMock(AlreadyMappedFieldCollectorInterface::class);
// Set up expected behavior for objectCollector
$objectCollection = new ObjectCollection();
$objectCollection->append(new ObjectCrate('contact', 'Contact'));
$this->objectCollector->method('getObjects')->willReturn($objectCollection);
// Set up expected behavior for fieldCollector
$fieldCollection = new FieldCollection([
new FieldCrate('1', 'email', 'text', []),
]);
$this->fieldCollector->method('getFields')->willReturn($fieldCollection);
parent::setUp();
}
/**
* @return array<FormExtensionInterface>
*/
protected function getExtensions(): array
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
FieldType::class => new FieldType(
$this->translator,
$this->objectCollector,
$this->fieldCollector,
$this->mappedFieldCollector
),
], []),
];
}
public function testFieldWidthDefaultValue(): void
{
$formData = [
'type' => 'text',
'addFieldWidth' => true,
'formId' => 1,
];
$form = $this->factory->create(FieldType::class, $formData);
$view = $form->createView();
$this->assertArrayHasKey('fieldWidth', $view);
$fieldWidth = $form->get('fieldWidth');
$this->assertEquals('100%', $fieldWidth->getData());
}
public function testFieldWidthChoices(): void
{
$formData = [
'type' => 'text',
'addFieldWidth' => true,
'formId' => 1,
];
$form = $this->factory->create(FieldType::class, $formData);
$view = $form->createView();
$expectedChoices = [
'100%' => 'mautic.form.field.form.field_width.one_hundred',
'75%' => 'mautic.form.field.form.field_width.seventy_five',
'66.66%' => 'mautic.form.field.form.field_width.sixty_six',
'50%' => 'mautic.form.field.form.field_width.fifty',
'33.33%' => 'mautic.form.field.form.field_width.thirty_three',
'25%' => 'mautic.form.field.form.field_width.twenty_five',
];
$this->assertArrayHasKey('fieldWidth', $view);
$fieldWidth = $view->children['fieldWidth'];
$choices = $fieldWidth->vars['choices'];
$this->assertCount(count($expectedChoices), $choices);
foreach ($choices as $choice) {
$this->assertArrayHasKey($choice->value, $expectedChoices);
$this->assertEquals($expectedChoices[$choice->value], $choice->label);
}
}
public function testFieldWidthCustomValue(): void
{
$formData = [
'type' => 'text',
'addFieldWidth' => true,
'fieldWidth' => '75%',
'formId' => 1,
];
$form = $this->factory->create(FieldType::class, $formData);
$fieldWidth = $form->get('fieldWidth');
$this->assertEquals('75%', $fieldWidth->getData());
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Form\Type;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Mautic\FormBundle\Form\Type\FormFieldConditionType;
use Mautic\FormBundle\Helper\PropertiesAccessor;
use Mautic\FormBundle\Model\FieldModel;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
final class FormFieldConditionTypeTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|FieldModel
*/
private MockObject $fieldModel;
/**
* @var MockObject|PropertiesAccessor
*/
private MockObject $propertiesAccessor;
/**
* @var MockObject&FormBuilderInterface<string|FormBuilderInterface>
*/
private MockObject $formBuilder;
private FormFieldConditionType $form;
protected function setUp(): void
{
$this->fieldModel = $this->createMock(FieldModel::class);
$this->propertiesAccessor = $this->createMock(PropertiesAccessor::class);
$this->formBuilder = $this->createMock(FormBuilderInterface::class);
$this->form = new FormFieldConditionType(
$this->fieldModel,
$this->propertiesAccessor
);
}
public function testBuildFormIfParentIsEmpty(): void
{
$options = [];
$this->fieldModel->expects($this->never())
->method('getSessionFields');
$this->propertiesAccessor->expects($this->never())
->method('getChoices');
$matcher = $this->exactly(3);
$this->formBuilder->expects($matcher)
->method('add')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('values', $parameters[0]);
$this->assertSame(ChoiceType::class, $parameters[1]);
$this->assertSame([
'choices' => [],
'multiple' => true,
'label' => false,
'attr' => [
'class' => 'form-control',
'data-show-on' => '{"formfield_conditions_any_0": "checked","formfield_conditions_expr": "notIn"}',
],
'required' => false,
], $parameters[2]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('any', $parameters[0]);
$this->assertSame(YesNoButtonGroupType::class, $parameters[1]);
$this->assertSame([
'label' => 'mautic.form.field.form.condition.any_value',
'attr' => [
'data-show-on' => '{"formfield_conditions_expr": "in"}',
],
'data' => false,
], $parameters[2]);
}
if (3 === $matcher->numberOfInvocations()) {
$this->assertSame('expr', $parameters[0]);
$this->assertSame(ChoiceType::class, $parameters[1]);
$this->assertSame([
'choices' => [
'mautic.core.operator.in' => 'in',
'mautic.core.operator.notin' => 'notIn',
],
'label' => false,
'placeholder' => false,
'attr' => [
'class' => 'form-control',
],
'required' => false,
], $parameters[2]);
}
return $this->formBuilder;
});
$this->form->buildForm($this->formBuilder, $options);
}
public function testBuildFormIfParentExists(): void
{
$options = [
'parent' => 'parent_field_id',
'formId' => 'form_id',
];
$this->fieldModel->expects($this->once())
->method('getSessionFields')
->with('form_id')
->willReturn(['parent_field_id' => ['some_field_props_here']]);
$this->propertiesAccessor->expects($this->once())
->method('getProperties')
->with(['some_field_props_here'])
->willReturn(['some_choice_here' => 'Some choice here']);
$this->propertiesAccessor->expects($this->once())
->method('getChoices')
->with(['some_choice_here' => 'Some choice here'])
->willReturn(['some_choice_here' => 'Some choice here']);
$matcher = $this->any();
$this->formBuilder->expects($matcher)->method('add')
->willReturnCallback(
function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('values', $parameters[0]);
$this->assertSame(ChoiceType::class, $parameters[1]);
$this->assertSame([
'choices' => ['some_choice_here' => 'Some choice here'],
'multiple' => true,
'label' => false,
'attr' => [
'class' => 'form-control',
'data-show-on' => '{"formfield_conditions_any_0": "checked","formfield_conditions_expr": "notIn"}',
],
'required' => false,
], $parameters[2]);
}
return $this->formBuilder;
}
);
$this->form->buildForm($this->formBuilder, $options);
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Form\Type;
use Mautic\FormBundle\Form\Type\FormFieldNumberType;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Test\TypeTestCase;
final class FormFieldNumberTypeTest extends TypeTestCase
{
/**
* @var MockObject|FormBuilderInterface
*/
private $formBuilder;
/**
* @var AbstractType<FormFieldNumberType>
*/
private $form;
protected function setUp(): void
{
parent::setUp();
$this->formBuilder = $this->createMock(FormBuilderInterface::class);
$this->form = new FormFieldNumberType();
}
public function testBuildFormIfParentIsEmpty(): void
{
$options = [
'data' => [
'precision' => 0,
],
];
$matcher = $this->exactly(2);
$this->formBuilder->expects($matcher)
->method('add')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('placeholder', $parameters[0]);
$this->assertSame(TextType::class, $parameters[1]);
$this->assertSame([
'label' => 'mautic.form.field.form.property_placeholder',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
'required' => false,
], $parameters[2]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('precision', $parameters[0]);
$this->assertSame(IntegerType::class, $parameters[1]);
$this->assertSame([
'label' => 'mautic.form.field.form.number_precision',
'label_attr' => ['class' => 'control-label'],
'data' => 0,
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.form.field.form.number_precision.tooltip',
],
'required' => false,
], $parameters[2]);
}
return $this->formBuilder;
});
$this->form->buildForm($this->formBuilder, $options);
}
public function testSubmitValidData(): void
{
$formData = [
'placeholder' => 'test',
'precision' => 1,
];
$form = $this->factory->create(FormFieldNumberType::class);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertNotEmpty($form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Form\Type;
use Mautic\FormBundle\Form\Type\FormFieldSliderType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
final class FormFieldSliderTypeTest extends TypeTestCase
{
protected function getExtensions(): array
{
return [
new ValidatorExtension(Validation::createValidator()),
new PreloadedExtension([
FormFieldSliderType::class => new FormFieldSliderType(),
], []),
];
}
public function testSubmitValidData(): void
{
$formData = [
'min' => 0,
'max' => 50,
'step' => 5,
];
$form = $this->factory->create(FormFieldSliderType::class);
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertNotEmpty($form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
public function testSubmitInvalidData(): void
{
$form = $this->factory->create(FormFieldSliderType::class);
$invalidData = [
'min' => 10,
'max' => 5,
'step' => 15,
];
$form->submit($invalidData);
$this->assertTrue($form->isSynchronized());
$this->assertFalse($form->isValid());
$errors = $form->getErrors(true);
$this->assertGreaterThan(0, count($errors));
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Mautic\FormBundle\Tests\Helper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Helper\FormFieldHelper;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class FormFieldHelperTest extends \PHPUnit\Framework\TestCase
{
/**
* @var FormFieldHelper
*/
protected $fixture;
protected function setUp(): void
{
$translatorMock = $this->createMock(Translator::class);
$validatorMock = $this->createMock(ValidatorInterface::class);
$this->fixture = new FormFieldHelper($translatorMock, $validatorMock);
}
#[\PHPUnit\Framework\Attributes\DataProvider('fieldProvider')]
public function testPopulateField($field, $value, $formHtml, $expectedValue, $message): void
{
$this->fixture->populateField($field, $value, 'mautic', $formHtml);
$this->assertEquals($expectedValue, $formHtml, $message);
}
/**
* @return array
*/
public static function fieldProvider()
{
return [
[
self::getField('First Name', 'text'),
'%22%2F%3E%3Cscript%3Ealert%280%29%3C%2Fscript%3E',
'<input value="" id="mauticform_input_mautic_firstname" />',
'<input id="mauticform_input_mautic_firstname" value="&quot;/&gt;alert(0)" />',
'Tags should be stripped from textet field values submitted via GET to prevent XSS.',
],
[
self::getField('First Name', 'text'),
'%22%20onfocus=%22alert(123)',
'<input value="" id="mauticform_input_mautic_firstname" />',
'<input id="mauticform_input_mautic_firstname" value="&quot; onfocus=&quot;alert(123)" />',
'Inline JS values should not be allowed via GET to prevent XSS.',
],
[
self::getField('Phone', 'tel'),
'+41 123 456 7890',
'<input value="" id="mauticform_input_mautic_phone" />',
'<input id="mauticform_input_mautic_phone" value="+41 123 456 7890" />',
'Phone number are populated properly',
],
[
self::getField('Description', 'textarea'),
'%22%2F%3E%3Cscript%3Ealert%280%29%3C%2Fscript%3E',
'<textarea id="mauticform_input_mautic_description"></textarea>',
'<textarea id="mauticform_input_mautic_description">&quot;/&gt;alert(0)</textarea>',
'Tags should be stripped from textarea field values submitted via GET to prevent XSS.',
],
[
self::getField('Description', 'textarea'),
'%22%20onfocus=%22alert(123)',
'<textarea id="mauticform_input_mautic_description"></textarea>',
'<textarea id="mauticform_input_mautic_description">&quot; onfocus=&quot;alert(123)</textarea>',
'Tags should be stripped from textarea field values submitted via GET to prevent XSS.',
],
[
self::getField('Checkbox Single', 'checkboxgrp'),
'myvalue',
'<input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Single').'1" value="myvalue"/><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Single').'2" value="notmyvalue"/>',
'<input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Single').'1" value="myvalue" checked /><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Single').'2" value="notmyvalue"/>',
'Single value checkbox groups should have their values set appropriately via GET.',
],
[
self::getField('Checkbox Multi', 'checkboxgrp'),
'myvalue%7Calsomyvalue',
'<input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'1" value="myvalue"/><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'2" value="alsomyvalue"/><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'3" value="notmyvalue"/>',
'<input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'1" value="myvalue" checked /><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'2" value="alsomyvalue" checked /><input id="mauticform_checkboxgrp_checkbox_'.self::getAliasFromName('Checkbox Multi').'3" value="notmyvalue"/>',
'Multi-value checkbox groups should have their values set appropriately via GET.',
],
[
self::getField('Radio Single', 'radiogrp'),
'myvalue',
'<input id="mauticform_radiogrp_radio_'.self::getAliasFromName('Radio Single').'1" value="myvalue"/><input id="mauticform_radiogrp_radio_'.self::getAliasFromName('Radio Single').'1" value="notmyvalue"/>',
'<input id="mauticform_radiogrp_radio_'.self::getAliasFromName('Radio Single').'1" value="myvalue" checked /><input id="mauticform_radiogrp_radio_'.self::getAliasFromName('Radio Single').'1" value="notmyvalue"/>',
'Single value radio groups should have their values set appropriately via GET.',
],
[
self::getField('Select', 'select'),
'myvalue',
'<select id="mauticform_input_mautic_select"><option value="myvalue">My Value</option></select>',
'<select id="mauticform_input_mautic_select"><option value="myvalue" selected="selected">My Value</option></select>',
'Select lists should have their values set appropriately via GET.',
],
];
}
/**
* @param string $name
* @param string $type
*
* @return Field
*/
protected static function getField($name, $type)
{
$field = new Field();
$field->setLabel($name);
$field->setAlias(self::getAliasFromName($name));
$field->setType($type);
return $field;
}
/**
* @param string $name
*
* @return string
*/
private static function getAliasFromName($name)
{
return strtolower(str_replace(' ', '', $name));
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Helper;
use Mautic\FormBundle\Helper\PropertiesAccessor;
use Mautic\FormBundle\Model\FormModel;
use PHPUnit\Framework\MockObject\MockObject;
final class PropertiesAccessorTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|FormModel
*/
private MockObject $formModel;
private PropertiesAccessor $propertiesAccessor;
protected function setUp(): void
{
$this->formModel = $this->createMock(FormModel::class);
$this->propertiesAccessor = new PropertiesAccessor(
$this->formModel
);
}
public function testGetPropertiesForCountryField(): void
{
$field = [
'type' => 'country',
'mappedField' => 'country',
];
$this->formModel->expects($this->once())
->method('getContactFieldPropertiesList')
->with('country')
->willReturn(['some_props_here']);
$this->assertSame(
['some_props_here'],
$this->propertiesAccessor->getProperties($field)
);
}
public function testGetPropertiesForSyncList(): void
{
$field = [
'type' => 'custom_select_a',
'mappedField' => 'contact_field_a',
'mappedObject' => 'contact',
'properties' => ['syncList' => true],
];
$this->formModel->expects($this->once())
->method('getContactFieldPropertiesList')
->with('contact_field_a')
->willReturn(['some_props_here']);
$this->assertSame(
['some_props_here'],
$this->propertiesAccessor->getProperties($field)
);
}
public function testGetPropertiesForTextField(): void
{
$field = [
'type' => 'custom_text_a',
'mappedField' => 'contact_field_a',
'mappedObject' => 'contact',
'properties' => ['syncList' => false],
];
$this->formModel->expects($this->never())
->method('getContactFieldPropertiesList');
$this->assertSame(
[],
$this->propertiesAccessor->getProperties($field)
);
}
public function testGetPropertiesForListField(): void
{
$field = [
'type' => 'custom_select_a',
'properties' => [
'syncList' => false,
'list' => ['list' => ['option_a' => 'Option A']],
],
];
$this->formModel->expects($this->never())
->method('getContactFieldPropertiesList');
$this->assertSame(
['option_a' => 'Option A'],
$this->propertiesAccessor->getProperties($field)
);
}
public function testGetPropertiesForOptionlistField(): void
{
$field = [
'type' => 'custom_select_a',
'properties' => [
'syncList' => false,
'optionlist' => ['list' => ['option_a' => 'Option A']],
],
];
$this->formModel->expects($this->never())
->method('getContactFieldPropertiesList');
$this->assertSame(
['option_a' => 'Option A'],
$this->propertiesAccessor->getProperties($field)
);
}
public function testGetChoicesForWellFormattedChoices(): void
{
$options = ['choice_a' => 'Choice A'];
$this->assertSame(
array_flip($options),
$this->propertiesAccessor->getChoices($options)
);
}
public function testGetChoicesForPipeFormattedChoices(): void
{
$options = 'Choice A|Choice B';
$this->assertSame(
['Choice A' => 'Choice A', 'Choice B' => 'Choice B'],
$this->propertiesAccessor->getChoices($options)
);
}
public function testGetChoicesForLabelValueArrayChoices(): void
{
$options = [
[
'label' => 'Choice A',
'value' => 'Value A',
],
];
$this->assertSame(
['Choice A' => 'Value A'],
$this->propertiesAccessor->getChoices($options)
);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Model;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
use Mautic\CoreBundle\Doctrine\Helper\TableSchemaHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\ThemeHelperInterface;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Collector\MappedObjectCollectorInterface;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\FormRepository;
use Mautic\FormBundle\Helper\FormFieldHelper;
use Mautic\FormBundle\Helper\FormUploader;
use Mautic\FormBundle\Model\ActionModel;
use Mautic\FormBundle\Model\FieldModel;
use Mautic\FormBundle\Model\FormModel;
use Mautic\LeadBundle\Helper\PrimaryCompanyHelper;
use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
class DeleteFormTest extends \PHPUnit\Framework\TestCase
{
public function testDelete(): void
{
$requestStack = $this->createMock(RequestStack::class);
$twigMock = $this->createMock(Environment::class);
$themeHelper = $this->createMock(ThemeHelperInterface::class);
$formActionModel = $this->createMock(ActionModel::class);
$formFieldModel = $this->createMock(FieldModel::class);
$fieldHelper = $this->createMock(FormFieldHelper::class);
$primaryCompanyHelper = $this->createMock(PrimaryCompanyHelper::class);
$leadFieldModel = $this->createMock(LeadFieldModel::class);
$formUploaderMock = $this->createMock(FormUploader::class);
$contactTracker = $this->createMock(ContactTracker::class);
$columnSchemaHelper = $this->createMock(ColumnSchemaHelper::class);
$tableSchemaHelper = $this->createMock(TableSchemaHelper::class);
$entityManager = $this->createMock(EntityManagerInterface::class);
$dispatcher = $this->createMock(EventDispatcher::class);
$formRepository = $this->createMock(FormRepository::class);
$form = $this->createMock(Form::class);
$mappedObjectCollector = $this->createMock(MappedObjectCollectorInterface::class);
$formModel = new FormModel(
$requestStack,
$twigMock,
$themeHelper,
$formActionModel,
$formFieldModel,
$fieldHelper,
$primaryCompanyHelper,
$leadFieldModel,
$formUploaderMock,
$contactTracker,
$columnSchemaHelper,
$tableSchemaHelper,
$mappedObjectCollector,
$entityManager,
$this->createMock(CorePermissions::class),
$dispatcher,
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class)
);
$matcher = $this->exactly(2);
$dispatcher->expects($matcher)
->method('hasListeners')->willReturnCallback(function (...$parameters) use ($matcher) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame('mautic.form_pre_delete', $parameters[0]);
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame('mautic.form_post_delete', $parameters[0]);
}
return false;
});
$entityManager->expects($this->once())
->method('getRepository')
->willReturn($formRepository);
$form->expects($this->exactly(2))
->method('getId')
->with()
->willReturn(1);
$formUploaderMock->expects($this->once())
->method('deleteFilesOfForm')
->with($form);
$formRepository->expects($this->once())
->method('deleteEntity')
->with($form);
$formModel->deleteEntity($form);
$this->assertSame(1, $form->deletedId);
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Mautic\FormBundle\Tests\Model;
use Doctrine\DBAL\Schema\Column;
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Model\FieldModel;
use Mautic\FormBundle\Model\FormModel;
use Symfony\Component\HttpFoundation\Request;
class FieldModelFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testDeleteFormFieldShouldRemoveTableColumn(): void
{
$formData = [
'name' => 'FormTest',
'description' => 'Form created via submission test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'email',
'alias' => 'email',
'mappedObject' => 'contact',
'mappedField' => 'email',
],
[
'label' => 'First name',
'type' => 'text',
'alias' => 'fname',
],
[
'label' => 'Last name',
'type' => 'text',
'alias' => 'lname',
],
[
'label' => 'Submit',
'type' => 'button',
],
],
];
// Create form.
$form = $this->createForm($formData);
/** @var ColumnSchemaHelper $helper */
$helper = $this->getContainer()->get('mautic.schema.helper.column');
// Table name to check the fields.
$name = 'form_results_'.$form->getId().'_'.$form->getAlias();
$schemaHelper = $helper->setName($name);
// The table will have four column, 'submission_id', 'form_id', 'email', and 'fname'.
$this->assertEquals(5, count($schemaHelper->getColumns()));
/** @var FieldModel $fieldModel */
$fieldModel = $this->getContainer()->get('mautic.form.model.field');
$ids = $this->getDeleteIds($fieldModel);
// Let's delete the 'First name' field.
$fieldModel->deleteEntities($ids);
$this->assertTrue($schemaHelper->checkColumnExists('email'), 'The table has \'email\' field.');
$this->assertFalse($schemaHelper->checkColumnExists('fname'), 'The table does not have the \'fname\' field.');
$this->assertFalse($schemaHelper->checkColumnExists('lname'), 'The table does not have the \'lname\' field.');
}
/**
* @param mixed[] $payload
*/
private function createForm(array $payload): Form
{
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
/** @var FormModel $formModel */
$formModel = $this->getContainer()->get('mautic.form.model.form');
return $formModel->getEntity($response['form']['id']);
}
/**
* @return mixed[]
*/
private function getDeleteIds(FieldModel $fieldModel): array
{
$fields = $fieldModel->getEntities([
'filter' => [
'force' => [
[
'column' => $fieldModel->getRepository()->getTableAlias().'.alias',
'expr' => 'in',
'value' => ['fname', 'lname'],
],
],
],
'ignore_paginator' => true,
]);
$ids = [];
foreach ($fields as $field) {
$ids[] = $field->getId();
}
return $ids;
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Mautic\FormBundle\Tests\Model;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Model\FieldModel;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class FieldModelTest extends TestCase
{
public function testGenerateAlias(): void
{
$connection = $this->createMock(Connection::class);
$platform = new class {
public function getReservedKeywordsList(): object
{
return new class {
public function isKeyword(): bool
{
return false;
}
};
}
public function isKeyword(): bool
{
return false;
}
};
$connection->method('getDatabasePlatform')
->willReturn($platform);
$leadFieldModel = $this->createMock(\Mautic\LeadBundle\Model\FieldModel::class);
$entityManager = $this->createMock(EntityManager::class);
$schemaHelper = $this->createMock(ColumnSchemaHelper::class);
$fieldModel = new FieldModel(
$leadFieldModel,
$entityManager,
$this->createMock(CorePermissions::class),
$this->createMock(EventDispatcherInterface::class),
$this->createMock(UrlGeneratorInterface::class),
$this->createMock(Translator::class),
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class),
$this->createMock(RequestStack::class),
$schemaHelper
);
$entityManager->expects($this->any())
->method('getConnection')
->willReturn($connection);
$aliases = [
'existed_alias',
'existed_alias_with_space',
];
$strings = [
'existed_alias1' => 'existed alias',
'not_existed' => 'not existed',
'existed_alias_with_space1' => 'existed alias with space',
'alias_test' => 'alias test',
];
foreach ($strings as $expected => $string) {
$alias = $fieldModel->generateAlias($string, $aliases);
$this->assertEquals($expected, $alias);
}
}
}

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Model\FormModel;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadField;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class FormModelFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testPopulateValuesWithGetParameters(): void
{
$formId = $this->createForm();
$crawler = $this->client->request(
Request::METHOD_GET,
"/s/forms/preview/{$formId}?email=testform@test.com&firstname=test&description=test-test&checkbox=val1|val3"
);
$inputValue = $crawler->filter('input[type=email]')->attr('value');
self::assertSame('testform@test.com', $inputValue);
$inputValue = $crawler->filter('input[type=text]')->attr('value');
self::assertSame('test', $inputValue);
$inputValue = $crawler->filter('textarea[name^=mauticform]')->html();
self::assertSame('test-test', $inputValue);
$inputValue = $crawler->filter('textarea[name^=mauticform]')->html();
self::assertSame('test-test', $inputValue);
$inputValue = $crawler->filter('input[value^=val1]')->attr('checked');
self::assertNotNull($inputValue, $crawler->html());
$inputValue = $crawler->filter('input[value^=val2]')->attr('checked');
self::assertNull($inputValue);
$inputValue = $crawler->filter('input[value^=val3]')->attr('checked');
self::assertNotNull($inputValue);
$this->createPage($formId);
$crawler = $this->client->request(Request::METHOD_GET, '/test-page?email=test%2Bpage@test.com&firstname=test');
$inputValue = $crawler->filter('input[type=email]')->attr('value');
self::assertSame('test+page@test.com', $inputValue);
$inputValue = $crawler->filter('input[type=text]')->attr('value');
self::assertSame('test', $inputValue);
}
private function createForm(): int
{
$formPayload = [
'name' => 'Test Form',
'formType' => 'standalone',
'description' => 'API test',
'fields' => [
[
'label' => 'firstname',
'alias' => 'firstname',
'type' => 'text',
],
[
'label' => 'email',
'alias' => 'email',
'type' => 'email',
'leadField' => 'email',
],
[
'label' => 'description',
'alias' => 'description',
'type' => 'textarea',
],
[
'label' => 'checkbox',
'alias' => 'checkbox',
'type' => 'checkboxgrp',
'properties' => [
'syncList' => 0,
'optionlist' => [
'list' => [
[
'label' => 'val1',
'value' => 'val1',
],
[
'label' => 'val2',
'value' => 'val2',
],
[
'label' => 'val3',
'value' => 'val3',
],
],
],
'labelAttributes' => null,
],
],
[
'label' => 'Submit',
'alias' => 'submit',
'type' => 'button',
],
],
'postAction' => 'return',
];
$this->client->request('POST', '/api/forms/new', $formPayload);
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
$response = json_decode($clientResponse->getContent(), true);
return $response['form']['id'];
}
private function createPage(int $formId): void
{
$pagePayload = [
'title' => 'Test Page',
'alias' => 'test-page',
'description' => 'This is my first page created via API.',
'isPublished' => true,
'customHtml' => '<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
<meta name="description" content="Test Page" />
</head>
<body>
<div class="container">
<div>{form='.$formId.'}</div>
</div>
</body>
</html>',
];
$this->client->request('POST', '/api/pages/new', $pagePayload);
$clientResponse = $this->client->getResponse();
$this->assertEquals(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
}
public function testLeadPopulateValuesWithLeadFields(): void
{
$multiselectFieldId = $this->createMultiselectLeadField();
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
$multiselectField = $fieldModel->getEntity($multiselectFieldId);
$fieldAlias = $multiselectField->getAlias();
$form = $this->createFormWithMultiselect($fieldAlias);
$formId = $form->getId();
$lead = new Lead();
$lead->setEmail('test@example.com');
$lead->addUpdatedField($fieldAlias, 'a|b');
$this->em->persist($lead);
$this->em->flush();
$this->logoutUser();
$contactTracker = $this->getContainer()->get('mautic.tracker.contact');
$contactTracker->setTrackedContact($lead);
$this->client->request('GET', "/form/{$formId}");
$formCrawler = $this->client->getCrawler();
$checkboxA = $formCrawler->filter('[id*="mauticform_checkboxgrp_checkbox_"][id$="_a0"]')->attr('checked');
$checkboxB = $formCrawler->filter('[id*="mauticform_checkboxgrp_checkbox_"][id$="_b1"]')->attr('checked');
$checkboxC = $formCrawler->filter('[id*="mauticform_checkboxgrp_checkbox_"][id$="_c2"]')->attr('checked');
$this->assertNotNull($checkboxA, 'Checkbox A should be preselected.');
$this->assertNotNull($checkboxB, 'Checkbox B should be preselected.');
$this->assertNull($checkboxC, 'Checkbox C should NOT be preselected.');
}
private function createFormWithMultiselect(string $leadFieldAlias): Form
{
$payload = [
'name' => 'Test Form',
'formType' => 'standalone',
'description' => 'API test',
'fields' => [
[
'label' => 'email',
'alias' => 'email',
'type' => 'email',
'mappedField' => 'email',
'mappedObject' => 'contact',
'isAutoFill' => true,
'showWhenValueExists' => false,
],
[
'label' => $leadFieldAlias,
'alias' => $leadFieldAlias,
'type' => 'checkboxgrp',
'mappedField' => $leadFieldAlias,
'mappedObject' => 'contact',
'isAutoFill' => true,
'showWhenValueExists' => true,
'properties' => [
'syncList' => 1,
'labelAttributes' => null,
],
],
[
'label' => 'Submit',
'alias' => 'submit',
'type' => 'button',
],
],
'postAction' => 'return',
];
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
/** @var FormModel $formModel */
$formModel = $this->getContainer()->get('mautic.form.model.form');
return $formModel->getEntity($response['form']['id']);
}
private function createMultiselectLeadField(): int
{
/** @var \Mautic\LeadBundle\Model\FieldModel $fieldModel */
$fieldModel = $this->getContainer()->get('mautic.lead.model.field');
$alias = 'test_multiselect_'.uniqid();
$field = new LeadField();
$field->setType('multiselect');
$field->setObject('lead');
$field->setAlias($alias);
$field->setName($alias);
$field->setLabel($alias);
$field->setGroup('core');
$properties = [
'list' => [
['label' => 'a', 'value' => 'a'],
['label' => 'b', 'value' => 'b'],
['label' => 'c', 'value' => 'c'],
],
];
$field->setProperties($properties);
$fieldModel->saveEntity($field);
return $field->getId();
}
}

View File

@@ -0,0 +1,776 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
use Mautic\CoreBundle\Doctrine\Helper\TableSchemaHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\ThemeHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\FormBundle\Collector\MappedObjectCollectorInterface;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\FormRepository;
use Mautic\FormBundle\Helper\FormFieldHelper;
use Mautic\FormBundle\Helper\FormUploader;
use Mautic\FormBundle\Model\ActionModel;
use Mautic\FormBundle\Model\FieldModel;
use Mautic\FormBundle\Model\FormModel;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Helper\PrimaryCompanyHelper;
use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;
class FormModelTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject&RequestStack
*/
private MockObject $requestStack;
/**
* @var MockObject&Environment
*/
private MockObject $twigMock;
/**
* @var MockObject&ThemeHelper
*/
private MockObject $themeHelper;
/**
* @var MockObject&ActionModel
*/
private MockObject $formActionModel;
/**
* @var MockObject&FieldModel
*/
private MockObject $formFieldModel;
/**
* @var MockObject&EventDispatcher
*/
private MockObject $dispatcher;
/**
* @var MockObject&Translator
*/
private MockObject $translator;
/**
* @var MockObject&EntityManager
*/
private MockObject $entityManager;
/**
* @var MockObject&FormUploader
*/
private MockObject $formUploaderMock;
/**
* @var MockObject&ColumnSchemaHelper
*/
private MockObject $columnSchemaHelper;
/**
* @var MockObject&TableSchemaHelper
*/
private MockObject $tableSchemaHelper;
/**
* @var MockObject&FormRepository
*/
private MockObject $formRepository;
/**
* @var MockObject&LeadFieldModel
*/
private MockObject $leadFieldModel;
/**
* @var MockObject&ContactTracker
*/
private MockObject $contactTracker;
/**
* @var MockObject&FormFieldHelper
*/
private MockObject $fieldHelper;
/**
* @var MockObject&PrimaryCompanyHelper
*/
private MockObject $primaryCompanyHelper;
/**
* @var MockObject&MappedObjectCollectorInterface
*/
private MockObject $mappedObjectCollector;
private FormModel $formModel;
protected function setUp(): void
{
$this->requestStack = $this->createMock(RequestStack::class);
$this->twigMock = $this->createMock(Environment::class);
$this->themeHelper = $this->createMock(ThemeHelper::class);
$this->formActionModel = $this->createMock(ActionModel::class);
$this->formFieldModel = $this->createMock(FieldModel::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->fieldHelper = $this->createMock(FormFieldHelper::class);
$this->primaryCompanyHelper = $this->createMock(PrimaryCompanyHelper::class);
$this->dispatcher = $this->createMock(EventDispatcher::class);
$this->translator = $this->createMock(Translator::class);
$this->entityManager = $this->createMock(EntityManager::class);
$this->formUploaderMock = $this->createMock(FormUploader::class);
$this->leadFieldModel = $this->createMock(LeadFieldModel::class);
$this->formRepository = $this->createMock(FormRepository::class);
$this->columnSchemaHelper = $this->createMock(ColumnSchemaHelper::class);
$this->tableSchemaHelper = $this->createMock(TableSchemaHelper::class);
$this->mappedObjectCollector = $this->createMock(MappedObjectCollectorInterface::class);
$this->entityManager->expects($this
->any())
->method('getRepository')
->willReturnMap(
[
[Form::class, $this->formRepository],
]
);
$this->formModel = new FormModel(
$this->requestStack,
$this->twigMock,
$this->themeHelper,
$this->formActionModel,
$this->formFieldModel,
$this->fieldHelper,
$this->primaryCompanyHelper,
$this->leadFieldModel,
$this->formUploaderMock,
$this->contactTracker,
$this->columnSchemaHelper,
$this->tableSchemaHelper,
$this->mappedObjectCollector,
$this->entityManager,
$this->createMock(CorePermissions::class),
$this->dispatcher,
$this->createMock(UrlGeneratorInterface::class),
$this->translator,
$this->createMock(UserHelper::class),
$this->createMock(LoggerInterface::class),
$this->createMock(CoreParametersHelper::class)
);
}
public function testSetFields(): void
{
$form = new Form();
$fields = $this->getTestFormFields();
$this->formModel->setFields($form, $fields);
$entityFields = $form->getFields()->toArray();
$newField = $entityFields[array_keys($fields)[0]];
/** @var Field $fileField */
$fileField = $entityFields[array_keys($fields)[1]];
/** @var Field $parentField */
$parentField = $entityFields[array_keys($fields)[2]];
/** @var Field $childField */
$childField = $entityFields[array_keys($fields)[3]];
/** @var Field $childField */
$newChildField = $entityFields[array_keys($fields)[4]];
$this->assertSame('email', $newField->getType());
$this->assertSame('email', $newField->getAlias());
$this->assertSame(1, $newField->getOrder());
$this->assertSame('file', $fileField->getType());
$this->assertSame('file', $fileField->getAlias());
$this->assertSame(2, $fileField->getOrder());
$this->assertSame('select', $parentField->getType());
$this->assertSame('parent', $parentField->getAlias());
$this->assertSame(3, $parentField->getOrder());
$this->assertSame('text', $childField->getType());
$this->assertSame('child', $childField->getAlias());
$this->assertSame(4, $childField->getOrder());
$this->assertSame('text', $newChildField->getType());
$this->assertSame('new_child', $newChildField->getAlias());
$this->assertSame(4, $newChildField->getOrder());
}
public function testGetComponentsFields(): void
{
$components = $this->formModel->getCustomComponents();
$this->assertArrayHasKey('fields', $components);
}
public function testGetComponentsActions(): void
{
$components = $this->formModel->getCustomComponents();
$this->assertArrayHasKey('actions', $components);
}
public function testGetComponentsValidators(): void
{
$components = $this->formModel->getCustomComponents();
$this->assertArrayHasKey('validators', $components);
}
public function testGetEntityForNotFoundContactField(): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$formField = new Field();
$formField->setMappedField('contactselect');
$formField->setMappedObject('contact');
$formField->setProperties(['syncList' => true]);
$fields->add($formField);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->willReturn(null);
$this->formModel->getEntity(5);
$this->assertSame(['syncList' => true], $formField->getProperties());
}
public function testGetEntityForNotLinkedSelectField(): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$formField = new Field();
$formField->setProperties(['syncList' => true]);
$fields->add($formField);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->never())
->method('getEntityByAlias');
$this->formModel->getEntity(5);
}
public function testGetEntityForNotSyncedSelectField(): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$formField = new Field();
$formField->setMappedField('contactselect');
$formField->setMappedObject('contact');
$formField->setProperties(['syncList' => false]);
$fields->add($formField);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->never())
->method('getEntityByAlias');
$this->formModel->getEntity(5);
}
public function testGetEntityForSyncedBooleanFieldFromNotLeadObject(): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$options = ['no' => 'lunch?', 'yes' => 'dinner?'];
$formField = new Field();
$formField->setMappedField('contactbool');
$formField->setMappedObject('unicorn');
$formField->setProperties(['syncList' => true]);
$fields->add($formField);
$contactField = new LeadField();
$contactField->setType('boolean');
$contactField->setProperties($options);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->never())
->method('getEntityByAlias');
$this->formModel->getEntity(5);
}
public function testGetEntityForSyncedBooleanField(): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$options = ['no' => 'lunch?', 'yes' => 'dinner?'];
$formField = new Field();
$formField->setMappedField('contactbool');
$formField->setMappedObject('contact');
$formField->setProperties(['syncList' => true]);
$fields->add($formField);
$contactField = new LeadField();
$contactField->setType('boolean');
$contactField->setProperties($options);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->with('contactbool')
->willReturn($contactField);
$this->formModel->getEntity(5);
$this->assertSame(['lunch?', 'dinner?'], $formField->getProperties()['list']['list']);
}
public function testGetEntityForSyncedCountryField(): void
{
$formField = $this->standardSyncListStaticFieldTest('country');
$this->assertArrayHasKey('Czech Republic', $formField->getProperties()['list']['list']);
}
public function testGetEntityForSyncedRegionField(): void
{
$formField = $this->standardSyncListStaticFieldTest('region');
$this->assertArrayHasKey('Canada', $formField->getProperties()['list']['list']);
}
public function testGetEntityForSyncedTimezoneField(): void
{
$formField = $this->standardSyncListStaticFieldTest('timezone');
$this->assertArrayHasKey('Africa', $formField->getProperties()['list']['list']);
}
public function testGetEntityForSyncedLocaleField(): void
{
$formField = $this->standardSyncListStaticFieldTest('locale');
$this->assertArrayHasKey('Czech (Czechia)', $formField->getProperties()['list']['list']);
}
/**
* @return array<string[]>
*/
public static function fieldTypeProvider(): array
{
return [
['select'],
['multiselect'],
['lookup'],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('fieldTypeProvider')]
public function testSyncListField(string $type): void
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$options = [
['label' => 'label1', 'value' => 'value1'],
['label' => 'label2', 'value' => 'value2'],
];
$formField = new Field();
$formField->setMappedField('contactfieldalias');
$formField->setMappedObject('contact');
$formField->setProperties(['syncList' => true]);
$contactField = new LeadField();
$contactField->setType($type);
$contactField->setProperties(['list' => $options]);
$fields->add($formField);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->with('contactfieldalias')
->willReturn($contactField);
$this->formModel->getEntity(5);
if ('lookup' === $type) {
$expectedList = [];
foreach ($options as $option) {
$expectedList[$option['value']] = $option['label'];
}
$this->assertSame($expectedList, $formField->getProperties()['list']['list']);
} else {
$this->assertSame($options, $formField->getProperties()['list']['list']);
}
}
private function standardSyncListStaticFieldTest(string $type): Field
{
$formEntity = $this->createMock(Form::class);
$fields = new ArrayCollection();
$formField = new Field();
$formField->setMappedField('contactfield');
$formField->setMappedObject('contact');
$formField->setProperties(['syncList' => true]);
$fields->add($formField);
$contactField = new LeadField();
$contactField->setType($type);
$formEntity->expects($this->exactly(2))
->method('getFields')
->willReturn($fields);
$this->formRepository->expects($this->once())
->method('getEntity')
->with(5)
->willReturn($formEntity);
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->with('contactfield')
->willReturn($contactField);
$this->formModel->getEntity(5);
return $formField;
}
public function testGetContactFieldPropertiesListWhenFieldNotFound(): void
{
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias');
$this->assertNull($this->formModel->getContactFieldPropertiesList('alias_a'));
}
public function testGetContactFieldPropertiesListWhenFieldFoundButNotList(): void
{
$field = new LeadField();
$field->setType('text');
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->willReturn($field);
$this->assertNull($this->formModel->getContactFieldPropertiesList('alias_a'));
}
public function testGetContactFieldPropertiesListWhenSelectFieldFound(): void
{
$field = new LeadField();
$field->setType('select');
$field->setProperties(['list' => ['choice_a' => 'Choice A']]);
$this->leadFieldModel->expects($this->once())
->method('getEntityByAlias')
->willReturn($field);
$this->assertSame(
['choice_a' => 'Choice A'],
$this->formModel->getContactFieldPropertiesList('alias_a')
);
}
public function testPopulateValuesWithLeadWithoutAutofill(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$emailField->setMappedField('email');
$emailField->setMappedObject('contact');
$emailField->setIsAutoFill(false);
$form->addField(123, $emailField);
$this->contactTracker->expects($this->never())
->method('getContact');
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLeadWithoutLeadObject(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$emailField->setMappedField('email');
$emailField->setMappedObject('unicorn');
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$this->contactTracker->expects($this->never())
->method('getContact');
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLeadWithoutLeadEntity(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$emailField->setMappedField('email');
$emailField->setMappedObject('contact');
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$this->contactTracker->expects($this->once())
->method('getContact')
->willReturn(null);
$this->fieldHelper->expects($this->never())
->method('populateField');
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLeadWithoutMappedField(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$this->contactTracker->expects($this->never())
->method('getContact');
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLeadWithEmptyLeadFieldValue(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$contact = new Lead();
$emailField->setMappedField('email');
$emailField->setMappedObject('contact');
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$this->contactTracker->method('getContact')
->willReturn($contact);
$this->fieldHelper->expects($this->never())
->method('populateField');
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLead(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$contact = new Lead();
$emailField->setMappedField('email');
$emailField->setMappedObject('contact');
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$contactCompanyData = [
'email' => 'john@doe.email',
];
$this->contactTracker->method('getContact')
->willReturn($contact);
$this->primaryCompanyHelper->method('getProfileFieldsWithPrimaryCompany')
->willReturn($contactCompanyData);
$this->fieldHelper->expects($this->once())
->method('populateField')
->with($emailField, 'john@doe.email', 'form-', $formHtml);
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithLeadWithSuffixEMail(): void
{
$formHtml = '<html>';
$form = new Form();
$emailField = new Field();
$contact = new Lead();
$emailField->setMappedField('email');
$emailField->setMappedObject('contact');
$emailField->setIsAutoFill(true);
$form->addField(123, $emailField);
$contactCompanyData = [
'email' => 'john+test@doe.email',
];
$this->contactTracker->method('getContact')
->willReturn($contact);
$this->primaryCompanyHelper->method('getProfileFieldsWithPrimaryCompany')
->willReturn($contactCompanyData);
$this->fieldHelper->expects($this->once())
->method('populateField')
->with($emailField, 'john+test@doe.email', 'form-', $formHtml);
$this->formModel->populateValuesWithLead($form, $formHtml);
}
public function testPopulateValuesWithCompany(): void
{
$formHtml = '<html>';
$form = new Form();
$companyname = new Field();
$contact = new Lead();
$companyname->setMappedField('companyname');
$companyname->setMappedObject('company');
$companyname->setIsAutoFill(true);
$form->addField(123, $companyname);
$contactCompanyData = [
'companyname' => 'Mautic',
];
$this->contactTracker->method('getContact')
->willReturn($contact);
$this->primaryCompanyHelper->method('getProfileFieldsWithPrimaryCompany')
->willReturn($contactCompanyData);
$this->fieldHelper->expects($this->once())
->method('populateField')
->with($companyname, 'Mautic', 'form-', $formHtml);
$this->formModel->populateValuesWithLead($form, $formHtml);
}
/**
* @return mixed[]
*/
private function getTestFormFields(): array
{
$fieldSession = 'mautic_'.sha1(uniqid((string) mt_rand(), true));
$fieldSession2 = 'mautic_'.sha1(uniqid((string) mt_rand(), true));
$fields[$fieldSession] = [
'label' => 'Email',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'email',
'type' => 'email',
'mappedField' => 'email',
'mappedObject' => 'contact',
'id' => $fieldSession,
];
$fields['file'] = [
'label' => 'File',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'file',
'type' => 'file',
'id' => 'file',
'allowed_file_size' => 1,
'allowed_file_extensions' => ['jpg', 'gif'],
];
$fields['123'] = [
'label' => 'Parent Field',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'parent',
'type' => 'select',
'id' => '123',
];
$fields['456'] = [
'label' => 'Child',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'child',
'type' => 'text',
'id' => '456',
'parent' => '123',
];
$fields[$fieldSession2] = [
'label' => 'New Child',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'new_child',
'type' => 'text',
'id' => $fieldSession2,
'parent' => '123',
];
return $fields;
}
}

View File

@@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Model;
use Mautic\CoreBundle\Exception\FileUploadException;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\FileUploader;
use Mautic\FormBundle\Crate\UploadFileCrate;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\Submission;
use Mautic\FormBundle\Helper\FormUploader;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class FormUploaderTest extends \PHPUnit\Framework\TestCase
{
private $formId1 = 1;
private $formId2 = 2;
private $uploadDir = __DIR__.'/DummyFiles';
#[\PHPUnit\Framework\Attributes\TestDox('Uploader uploads files correctly')]
public function testSuccessfulUploadFiles(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$file1Mock = $this->createMock(UploadedFile::class);
$file2Mock = $this->createMock(UploadedFile::class);
$form1Mock = $this->createMock(Form::class);
$field1Mock = $this->createMock(Field::class);
$form2Mock = $this->createMock(Form::class);
$field2Mock = $this->createMock(Field::class);
$coreParametersHelperMock->expects($this->exactly(2))
->method('get')
->with('form_upload_dir')
->willReturn($this->uploadDir);
$form1Mock->expects($this->once())
->method('getId')
->with()
->willReturn($this->formId1);
$field1Mock->expects($this->once())
->method('getId')
->with()
->willReturn('fieldId1');
$field1Mock->expects($this->once())
->method('getForm')
->with()
->willReturn($form1Mock);
$field1Mock->expects($this->once())
->method('getAlias')
->with()
->willReturn('file1');
$form2Mock->expects($this->once())
->method('getId')
->with()
->willReturn($this->formId2);
$field2Mock->expects($this->once())
->method('getId')
->with()
->willReturn('fieldId2');
$field2Mock->expects($this->once())
->method('getForm')
->with()
->willReturn($form2Mock);
$field2Mock->expects($this->once())
->method('getAlias')
->with()
->willReturn('file2');
$filesToUpload = new UploadFileCrate();
$filesToUpload->addFile($file1Mock, $field1Mock);
$filesToUpload->addFile($file2Mock, $field2Mock);
$submission = new Submission();
$submission->setResults(['key' => 'value']);
$path1 = $this->uploadDir.'/1/fieldId1';
$path2 = $this->uploadDir.'/2/fieldId2';
$matcher = $this->exactly(2);
$fileUploaderMock->expects($matcher)
->method('upload')->willReturnCallback(function (...$parameters) use ($matcher, $path1, $file1Mock, $path2, $file2Mock) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($path1, $parameters[0]);
$this->assertSame($file1Mock, $parameters[1]);
return 'upload1.jpg';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($path2, $parameters[0]);
$this->assertSame($file2Mock, $parameters[1]);
return 'upload2.txt';
}
});
$formUploader->uploadFiles($filesToUpload, $submission);
$expected = [
'key' => 'value',
'file1' => 'upload1.jpg',
'file2' => 'upload2.txt',
];
$this->assertSame($expected, $submission->getResults());
}
#[\PHPUnit\Framework\Attributes\TestDox('Uploader delete uploaded file if anz error occures')]
public function testUploadFilesWithError(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$file1Mock = $this->createMock(UploadedFile::class);
$file2Mock = $this->createMock(UploadedFile::class);
$form1Mock = $this->createMock(Form::class);
$field1Mock = $this->createMock(Field::class);
$form2Mock = $this->createMock(Form::class);
$field2Mock = $this->createMock(Field::class);
$coreParametersHelperMock->expects($this->exactly(2))
->method('get')
->with('form_upload_dir')
->willReturn($this->uploadDir);
$form1Mock->expects($this->once())
->method('getId')
->willReturn($this->formId1);
$field1Mock->expects($this->once())
->method('getId')
->willReturn('fieldId1');
$field1Mock->expects($this->once())
->method('getForm')
->willReturn($form1Mock);
$field1Mock->expects($this->once())
->method('getAlias')
->willReturn('file1');
$form2Mock->expects($this->once())
->method('getId')
->willReturn($this->formId2);
$field2Mock->expects($this->once())
->method('getId')
->willReturn('fieldId2');
$field2Mock->expects($this->once())
->method('getForm')
->willReturn($form2Mock);
$field2Mock->expects($this->once())
->method('getAlias')
->willReturn('file2');
$filesToUpload = new UploadFileCrate();
$filesToUpload->addFile($file1Mock, $field1Mock);
$filesToUpload->addFile($file2Mock, $field2Mock);
$submission = new Submission();
$submission->setResults(['key' => 'value']);
$path1 = $this->uploadDir.'/1/fieldId1';
$path2 = $this->uploadDir.'/2/fieldId2';
$matcher = $this->exactly(2);
$fileUploaderMock->expects($matcher)
->method('upload')->willReturnCallback(function (...$parameters) use ($matcher, $path1, $file1Mock, $path2, $file2Mock) {
if (1 === $matcher->numberOfInvocations()) {
$this->assertSame($path1, $parameters[0]);
$this->assertSame($file1Mock, $parameters[1]);
return 'upload1.jpg';
}
if (2 === $matcher->numberOfInvocations()) {
$this->assertSame($path2, $parameters[0]);
$this->assertSame($file2Mock, $parameters[1]);
throw new FileUploadException();
}
});
$fileUploaderMock->expects($this->once())
->method('delete')
->with($this->uploadDir.'/1/fieldId1/upload1.jpg');
$this->expectException(FileUploadException::class);
$this->expectExceptionMessage('file2');
$formUploader->uploadFiles($filesToUpload, $submission);
$expected = [
'key' => 'value',
'file1' => 'upload1.jpg',
'file2' => 'upload2.txt',
];
$this->assertSame($expected, $submission->getResults());
}
#[\PHPUnit\Framework\Attributes\TestDox('Uploader do nothing if no files for upload provided')]
public function testNoFilesUploadFiles(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$fileUploaderMock->expects($this->never())
->method('upload');
$fileUploaderMock->expects($this->never())
->method('delete');
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$filesToUpload = new UploadFileCrate();
$submission = new Submission();
$formUploader->uploadFiles($filesToUpload, $submission);
}
#[\PHPUnit\Framework\Attributes\TestDox('Uploader returs correct path for file')]
public function testGetCompleteFilePath(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$coreParametersHelperMock->expects($this->once())
->method('get')
->with('form_upload_dir')
->willReturn($this->uploadDir);
$formMock = $this->createMock(Form::class);
$formMock->expects($this->once())
->method('getId')
->with()
->willReturn($this->formId1);
$fieldMock = $this->createMock(Field::class);
$fieldMock->expects($this->once())
->method('getId')
->with()
->willReturn('fieldId1');
$fieldMock->expects($this->once())
->method('getForm')
->with()
->willReturn($formMock);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$actual = $formUploader->getCompleteFilePath($fieldMock, 'fileName');
$this->assertSame($this->uploadDir.'/1/fieldId1/fileName', $actual);
}
#[\PHPUnit\Framework\Attributes\TestDox('Uploader delete files correctly')]
public function testDeleteAllFilesOfFormField(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$fileUploaderMock->expects($this->once())
->method('delete')
->with($this->uploadDir.'/1/fieldId1');
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$coreParametersHelperMock->expects($this->once())
->method('get')
->with('form_upload_dir')
->willReturn($this->uploadDir);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$formMock = $this->createMock(Form::class);
$formMock->expects($this->once())
->method('getId')
->with()
->willReturn($this->formId1);
$fieldMock = $this->createMock(Field::class);
$fieldMock->expects($this->once())
->method('getId')
->with()
->willReturn('fieldId1');
$fieldMock->expects($this->once())
->method('getForm')
->with()
->willReturn($formMock);
$fieldMock->expects($this->once())
->method('isFileType')
->with()
->willReturn(true);
$formUploader->deleteAllFilesOfFormField($fieldMock);
}
public function testDeleteFilesOfForm(): void
{
$fileUploaderMock = $this->createMock(FileUploader::class);
$formMock = $this->createMock(Form::class);
$coreParametersHelperMock = $this->createMock(CoreParametersHelper::class);
$fileUploaderMock
->method('delete')
->with($this->uploadDir.'/1');
$coreParametersHelperMock->expects($this->exactly(2))
->method('get')
->with('form_upload_dir')
->willReturn($this->uploadDir);
$formMock->expects($this->exactly(2))
->method('getId')
->willReturnOnConsecutiveCalls($this->formId1, null);
$formUploader = new FormUploader($fileUploaderMock, $coreParametersHelperMock);
$formUploader->deleteFilesOfForm($formMock);
$formMock->deletedId = $this->formId1;
$formUploader->deleteFilesOfForm($formMock);
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Mautic\FormBundle\Tests\Model;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\LeadBundle\Entity\Company;
use Mautic\LeadBundle\Entity\Lead;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class SubmissionModelFunctionalTest extends MauticMysqlTestCase
{
protected $useCleanupRollback = false;
public function testSaveSubmissionChangeCompanyField(): void
{
[$formId, $formAlias] = $this->createFormWithCompanies();
$this->submitFormWithCompanies($formId, $formAlias, 'test@acquia.cz', 'Luk', 'Doe', 'Acquia', 'Keplerova');
// Check the address.
$companyRepository = $this->em->getRepository(Company::class);
$companiesOriginal = $companyRepository->findBy(['address1' => 'Keplerova']);
Assert::assertCount(1, $companiesOriginal);
// Create contact with the same company but different address.
$this->submitFormWithCompanies($formId, $formAlias, 'test2@acquia.cz', 'Luk', 'Syk', 'Acquia', 'Krejpskeho');
// Check that the address is changed.
$companiesOld = $companyRepository->findBy(['address1' => 'Keplerova']);
Assert::assertCount(0, $companiesOld);
$companiesNew = $companyRepository->findBy(['address1' => 'Krejpskeho']);
Assert::assertCount(1, $companiesNew);
}
public function testSaveSubmissionChangeContactField(): void
{
[$formId, $formAlias] = $this->createFormWithoutCompanies();
$this->submitFormWithoutCompanies($formId, $formAlias, 'test@acquia.cz', 'Luk', 'Doe Smith');
// Check the contact.
$contactRepository = $this->em->getRepository(Lead::class);
$contactsOriginal = $contactRepository->findBy(['lastname' => 'Doe Smith']);
Assert::assertCount(1, $contactsOriginal);
// Create contact with the same email but different lastname.
$this->submitFormWithoutCompanies($formId, $formAlias, 'test@acquia.cz', 'Luk', 'Sykora');
// Check that the address is changed.
$contactsOld = $contactRepository->findBy(['lastname' => 'Doe Smith']);
Assert::assertCount(0, $contactsOld);
$contactsNew = $contactRepository->findBy(['lastname' => 'Sykora']);
Assert::assertCount(1, $contactsNew);
}
/**
* @return mixed[]
*/
private function createFormWithCompanies(): array
{
$payload = [
'name' => 'FormTest',
'description' => 'Form created via submission test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'email',
'alias' => 'email',
'mappedObject' => 'contact',
'mappedField' => 'email',
],
[
'label' => 'First Name',
'type' => 'text',
'alias' => 'firstname',
'mappedObject' => 'contact',
'mappedField' => 'firstname',
],
[
'label' => 'Last Name',
'type' => 'text',
'alias' => 'lastname',
'mappedObject' => 'contact',
'mappedField' => 'lastname',
],
[
'label' => 'Company',
'type' => 'text',
'alias' => 'companyname',
'mappedObject' => 'company',
'mappedField' => 'companyname',
],
[
'label' => 'Company Address',
'type' => 'text',
'alias' => 'companyaddress1',
'mappedObject' => 'company',
'mappedField' => 'companyaddress1',
],
[
'label' => 'Submit',
'type' => 'button',
],
],
];
return $this->createForm($payload);
}
/**
* @return mixed[]
*/
private function createFormWithoutCompanies(): array
{
$payload = [
'name' => 'FormTest',
'description' => 'Form created via submission test',
'formType' => 'standalone',
'isPublished' => true,
'fields' => [
[
'label' => 'Email',
'type' => 'email',
'alias' => 'email',
'mappedObject' => 'contact',
'mappedField' => 'email',
],
[
'label' => 'First Name',
'type' => 'text',
'alias' => 'firstname',
'mappedObject' => 'contact',
'mappedField' => 'firstname',
],
[
'label' => 'Last Name',
'type' => 'text',
'alias' => 'lastname',
'mappedObject' => 'contact',
'mappedField' => 'lastname',
],
],
];
return $this->createForm($payload);
}
/**
* @param mixed[] $payload
*
* @return array{int,string}
*/
private function createForm(array $payload): array
{
$this->client->request(Request::METHOD_POST, '/api/forms/new', $payload);
$clientResponse = $this->client->getResponse();
$response = json_decode($clientResponse->getContent(), true);
$formId = $response['form']['id'];
$formAlias = $response['form']['alias'];
Assert::assertSame(Response::HTTP_CREATED, $clientResponse->getStatusCode(), $clientResponse->getContent());
return [$formId, $formAlias];
}
private function submitFormWithCompanies(int $formId, string $formAlias, string $email, string $firstname, string $lastname, string $company, string $companyAddress): void
{
$values = [
'mauticform[email]' => $email,
'mauticform[firstname]' => $firstname,
'mauticform[lastname]' => $lastname,
'mauticform[companyname]' => $company,
'mauticform[companyaddress1]' => $companyAddress,
];
$this->submitForm($formId, $formAlias, $values);
}
private function submitFormWithoutCompanies(int $formId, string $formAlias, string $email, string $firstname, string $lastname): void
{
$values = [
'mauticform[email]' => $email,
'mauticform[firstname]' => $firstname,
'mauticform[lastname]' => $lastname,
];
$this->submitForm($formId, $formAlias, $values);
}
/**
* @param array<string,string> $values
*/
private function submitForm(int $formId, string $formAlias, array $values): void
{
$crawler = $this->client->request(Request::METHOD_GET, "/form/{$formId}");
$this->assertResponseIsSuccessful();
$formCrawler = $crawler->filter('form[id=mauticform_'.$formAlias.']');
$this::assertCount(1, $formCrawler, $this->client->getResponse()->getContent());
$form = $formCrawler->form();
$form->setValues($values);
$this->client->submit($form);
Assert::assertTrue($this->client->getResponse()->isOk(), $this->client->getResponse()->getContent());
}
}

View File

@@ -0,0 +1,635 @@
<?php
namespace Mautic\FormBundle\Tests\Model;
use Doctrine\ORM\EntityManager;
use Mautic\CampaignBundle\Membership\MembershipManager;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\CsvHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\CoreBundle\Twig\Helper\DateHelper;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\Entity\Submission;
use Mautic\FormBundle\Entity\SubmissionRepository;
use Mautic\FormBundle\Event\Service\FieldValueTransformer;
use Mautic\FormBundle\Event\SubmissionEvent;
use Mautic\FormBundle\Helper\FormFieldHelper;
use Mautic\FormBundle\Helper\FormUploader;
use Mautic\FormBundle\Model\FormModel;
use Mautic\FormBundle\Model\SubmissionModel;
use Mautic\FormBundle\Validator\UploadFieldValidator;
use Mautic\LeadBundle\Deduplicate\ContactMerger;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadRepository;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\FieldModel as LeadFieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\LeadBundle\Tracker\Service\DeviceTrackingService\DeviceTrackingServiceInterface;
use Mautic\PageBundle\Model\PageModel;
use Mautic\UserBundle\Entity\User;
use Monolog\Logger;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
class SubmissionModelTest extends \PHPUnit\Framework\TestCase
{
/**
* @var MockObject|IpLookupHelper
*/
private MockObject $ipLookupHelper;
/**
* @var MockObject|Environment
*/
private MockObject $twigMock;
/**
* @var MockObject|FormModel
*/
private MockObject $formModel;
/**
* @var MockObject|PageModel
*/
private MockObject $pageModel;
/**
* @var MockObject|LeadModel
*/
private MockObject $leadModel;
/**
* @var MockObject|CampaignModel
*/
private MockObject $campaignModel;
/**
* @var MockObject|MembershipManager
*/
private MockObject $membershipManager;
/**
* @var MockObject|LeadFieldModel
*/
private MockObject $leadFieldModel;
/**
* @var MockObject|CompanyModel
*/
private MockObject $companyModel;
/**
* @var MockObject|FormFieldHelper
*/
private MockObject $fieldHelper;
/**
* @var MockObject|EventDispatcherInterface
*/
private MockObject $dispatcher;
/**
* @var MockObject|Translator
*/
private MockObject $translator;
private DateHelper $dateHelper;
/**
* @var MockObject|UserHelper
*/
private MockObject $userHelper;
/**
* @var MockObject&FieldsWithUniqueIdentifier
*/
private MockObject $fieldsWithUniqueIdentifier;
/**
* @var MockObject|EntityManager
*/
private MockObject $entityManager;
/**
* @var MockObject|SubmissionRepository
*/
private MockObject $submissioRepository;
/**
* @var MockObject|LeadRepository
*/
private MockObject $leadRepository;
/**
* @var MockObject|Logger
*/
private MockObject $mockLogger;
/**
* @var MockObject|UploadFieldValidator
*/
private MockObject $uploadFieldValidatorMock;
/**
* @var MockObject|FormUploader
*/
private MockObject $formUploaderMock;
/**
* @var MockObject|DeviceTrackingServiceInterface
*/
private MockObject $deviceTrackingService;
/**
* @var MockObject|UploadedFile
*/
private MockObject $file1Mock;
/**
* @var MockObject|RouterInterface
*/
private MockObject $router;
/**
* @var MockObject|ContactTracker
*/
private MockObject $contactTracker;
/**
* @var MockObject|ContactMerger
*/
private MockObject $contactMerger;
private SubmissionModel $submissionModel;
/**
* @var \ReflectionClass<SubmissionModel>
*/
private \ReflectionClass $submissionModelReflection;
protected function setUp(): void
{
parent::setUp();
$this->ipLookupHelper = $this->createMock(IpLookupHelper::class);
$this->twigMock = $this->createMock(Environment::class);
$this->formModel = $this->createMock(FormModel::class);
$this->pageModel = $this->createMock(PageModel::class);
$this->leadModel = $this->createMock(LeadModel::class);
$this->campaignModel = $this->createMock(CampaignModel::class);
$this->membershipManager = $this->createMock(MembershipManager::class);
$this->leadFieldModel = $this->createMock(LeadFieldModel::class);
$this->companyModel = $this->createMock(CompanyModel::class);
$this->fieldHelper = $this->createMock(FormFieldHelper::class);
$this->dispatcher = $this->createMock(EventDispatcherInterface::class);
$this->translator = $this->createMock(Translator::class);
$this->dateHelper = new DateHelper(
'Y-m-d H:i:s',
'Y-m-d H:i',
'Y-m-d',
'H:i',
$this->translator,
$this->createMock(CoreParametersHelper::class)
);
$this->userHelper = $this->createMock(UserHelper::class);
$this->fieldsWithUniqueIdentifier = $this->createMock(FieldsWithUniqueIdentifier::class);
$this->entityManager = $this->createMock(EntityManager::class);
$this->submissioRepository = $this->createMock(SubmissionRepository::class);
$this->leadRepository = $this->createMock(LeadRepository::class);
$this->mockLogger = $this->createMock(Logger::class);
$this->uploadFieldValidatorMock = $this->createMock(UploadFieldValidator::class);
$this->formUploaderMock = $this->createMock(FormUploader::class);
$this->deviceTrackingService = $this->createMock(DeviceTrackingServiceInterface::class);
$this->file1Mock = $this->createMock(UploadedFile::class);
$this->router = $this->createMock(RouterInterface::class);
$this->contactTracker = $this->createMock(ContactTracker::class);
$this->contactMerger = $this->createMock(ContactMerger::class);
$this->fieldHelper->method('getFieldFilter')->willReturn('string');
$this->submissionModel = new SubmissionModel(
$this->ipLookupHelper,
$this->twigMock,
$this->formModel,
$this->pageModel,
$this->leadModel,
$this->campaignModel,
$this->membershipManager,
$this->leadFieldModel,
$this->companyModel,
$this->fieldHelper,
$this->uploadFieldValidatorMock,
$this->formUploaderMock,
$this->deviceTrackingService,
new FieldValueTransformer($this->router),
$this->dateHelper,
$this->contactTracker,
$this->contactMerger,
$this->fieldsWithUniqueIdentifier,
$this->entityManager,
$this->createMock(CorePermissions::class),
$this->dispatcher,
$this->createMock(UrlGeneratorInterface::class),
$this->translator,
$this->userHelper,
$this->mockLogger,
$this->createMock(CoreParametersHelper::class)
);
$this->submissionModelReflection = new \ReflectionClass($this->submissionModel);
}
public function testSaveSubmission(): void
{
$this->contactTracker->expects($this->any())
->method('getContact')
->willReturn(new Lead());
$this->userHelper->expects($this->any())
->method('getUser')
->willReturn(new User());
$mockLeadField['email'] = [
'label' => 'Email',
'alias' => 'email',
'type' => 'email',
'group' => 'core',
'group_label' => 'Core',
'defaultValue' => '',
'properties' => [],
];
$this->fieldsWithUniqueIdentifier->expects($this->any())
->method('getFieldsWithUniqueIdentifier')
->willReturn(['eyJpc1B1Ymxpc2hlZCI6dHJ1ZSwiaXNVbmlxdWVJZGVudGlmZXIiOnRydWUsIm9iamVjdCI6ImxlYWQifQ==' => ['email' => 'Email']]);
$this->leadFieldModel->expects($this->any())
->method('getFieldListWithProperties')
->willReturn($mockLeadField);
$this->companyModel->method('fetchCompanyFields')->willReturn([]);
$this->entityManager->expects($this->any())
->method('getRepository')
->willReturnMap(
[
[Lead::class, $this->leadRepository],
[Submission::class, $this->submissioRepository],
]
);
$this->leadRepository->expects($this->any())
->method('getLeadsByUniqueFields')
->willReturn(null);
$this->file1Mock->expects($this->any())
->method('getClientOriginalName')
->willReturn('test.jpg');
$this->router->expects($this->any())
->method('generate')
->willReturn('test.jpg');
$this->uploadFieldValidatorMock->expects($this->any())
->method('processFileValidation')
->willReturn($this->file1Mock);
$this->ipLookupHelper->expects($this->any())
->method('getIpAddress')
->willReturn(new IpAddress());
$request = new Request();
$request->setMethod('POST');
$formData = [
'var_name_1' => 'value 1',
'var_name_2' => 'value 2',
'email' => 'test@email.com',
'file' => 'test.jpg',
'submit' => '',
'formId' => 1,
'return' => '',
'formName' => 'testform',
'formid' => 1,
];
$post = $formData;
$server = $request->server->all();
$form = new Form();
$fields = $this->getTestFormFields();
$formModel = new class extends FormModel {
public function __construct()
{
}
};
$formModel->setFields($form, $fields);
$submissionEvent = $this->submissionModel->saveSubmission($post, $server, $form, $request, true)['submission'];
$this->assertInstanceOf(SubmissionEvent::class, $submissionEvent);
$tokens = $submissionEvent->getTokens();
$this->assertEquals($formData['email'], $tokens['{formfield=email}']);
$this->assertEquals($formData['file'], $tokens['{formfield=file}']);
$this->assertSame(['email' => 'test@email.com'], $submissionEvent->getContactFieldMatches());
$this->assertFalse($this->submissionModel->saveSubmission($post, $server, $form, $request));
}
public function testNormalizeValues(): void
{
$reflection = new \ReflectionClass(SubmissionModel::class);
$method = $reflection->getMethod('normalizeValue');
$method->setAccessible(true);
$fieldSession = 'mautic_'.sha1(uniqid((string) mt_rand(), true));
$fields[$fieldSession] = [
'label' => 'Email',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'email',
'type' => 'email',
'mappedField' => 'email',
'mappedObject' => 'contact',
'id' => $fieldSession,
];
$field = new Field();
$this->assertEquals('', $method->invokeArgs($this->submissionModel, ['', $field]));
$this->assertEquals(1, $method->invokeArgs($this->submissionModel, [1, $field]));
$this->assertEquals('1, 2', $method->invokeArgs($this->submissionModel, [[1, 2], $field]));
// field wiht list
$field = new Field();
$field->setProperties(
[
'list' => [
'list' => [
[
'label' => 'First',
'value' => 1,
],
[
'label' => 'Second',
'value' => 2,
],
],
],
]
);
$this->assertEquals('', $method->invokeArgs($this->submissionModel, ['', $field]));
$this->assertEquals('First', $method->invokeArgs($this->submissionModel, [1, $field]));
$this->assertEquals('First, Second', $method->invokeArgs($this->submissionModel, [[1, 2], $field]));
}
/**
* @return mixed[]
*/
private function getTestFormFields(): array
{
$fieldSession = 'mautic_'.sha1(uniqid((string) mt_rand(), true));
$fields[$fieldSession] = [
'label' => 'Email',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'email',
'type' => 'email',
'mappedField' => 'email',
'mappedObject' => 'contact',
'id' => $fieldSession,
];
$fields['file'] = [
'label' => 'File',
'showLabel' => 1,
'saveResult' => 1,
'defaultValue' => false,
'alias' => 'file',
'type' => 'file',
'id' => 'file',
'allowed_file_size' => 1,
'allowed_file_extensions' => ['jpg', 'gif'],
];
return $fields;
}
private function setUpExport(): void
{
$this->formModel->expects($this->any())
->method('getCustomComponents')
->willReturn(['viewOnlyFields' => ['button', 'captcha', 'freetext']]);
$this->submissioRepository->expects($this->any())
->method('getEntities')
->willReturn([]);
$this->entityManager->expects($this->any())
->method('getRepository')
->willReturn($this->submissioRepository);
}
public function testExportResultsCsv(): void
{
$this->setUpExport();
$response = $this->submissionModel->exportResults('csv', new Form(), []);
$this->assertSame($response::class, \Symfony\Component\HttpFoundation\StreamedResponse::class);
$this->assertStringContainsString('.csv', $response->headers->get('Content-Disposition'));
$this->assertSame('0', $response->headers->get('Expires'));
}
public function testExportResultsExcel(): void
{
$this->setUpExport();
$response = $this->submissionModel->exportResults('xlsx', new Form(), []);
$this->assertSame($response::class, \Symfony\Component\HttpFoundation\StreamedResponse::class);
$this->assertStringContainsString('.xlsx', $response->headers->get('Content-Disposition'));
$this->assertSame('0', $response->headers->get('Expires'));
}
private function mockTranslation(): void
{
$values = ['Submission ID', 'Contact ID', 'Date Submitted', 'IP address', 'Referrer', 'Form ID'];
$this->translator->expects($this->any())
->method('trans')
->with($this->anything())
->willReturnCallback(fn ($text) => match ($text) {
'mautic.form.report.submission.id' => $values[0],
'mautic.lead.report.contact_id' => $values[1],
'mautic.form.result.thead.date' => $values[2],
'mautic.core.ipaddress' => $values[3],
'mautic.form.result.thead.referrer' => $values[4],
'mautic.form.report.form_id' => $values[5],
default => null,
});
}
/**
* @throws \ReflectionException
*/
public function getAccessibleReflectionMethod(string $name): \ReflectionMethod
{
$method = $this->submissionModelReflection->getMethod($name);
$method->setAccessible(true);
return $method;
}
public function testGetExportHeader(): void
{
$form = new Form();
$field = new Field();
$field2 = new Field();
$field->setLabel('Email');
$field2->setType('text');
$field2->setLabel('Click');
$field2->setType('button');
$form->addField('email', $field);
$form->addField('button', $field2);
$viewOnlyFields = ['button', 'captcha', 'freetext'];
$expectedHeader = ['Submission ID', 'Contact ID', 'Date Submitted', 'IP address', 'Referrer', 'Email'];
$this->mockTranslation();
try {
$getExportHeaderRef = $this->getAccessibleReflectionMethod('getExportHeader');
$header = $getExportHeaderRef->invokeArgs($this->submissionModel, [$form, $viewOnlyFields]);
} catch (\ReflectionException $e) {
$this->fail($e->getMessage());
}
$this->assertCount(6, $header);
$this->assertSame($expectedHeader, $header);
$this->assertNotContains('Click', $header);
}
public function testGetExportHeaderForPage(): void
{
$expectedHeader = ['Submission ID', 'Contact ID', 'Form ID', 'Date Submitted', 'IP address', 'Referrer'];
$this->mockTranslation();
try {
$getExportHeaderForPageRef = $this->getAccessibleReflectionMethod('getExportHeaderForPage');
$header1 = $getExportHeaderForPageRef->invokeArgs($this->submissionModel, []);
$header2 = $getExportHeaderForPageRef->invokeArgs($this->submissionModel, ['xlsx']);
} catch (\ReflectionException $e) {
$this->fail($e->getMessage());
}
$this->assertCount(6, $header1);
$this->assertCount(5, $header2);
$this->assertSame($expectedHeader, $header1);
$this->assertNotContains('Form ID', $header2);
}
public function testPutCsvExportRow(): void
{
$tmpFile = tempnam(sys_get_temp_dir(), 'mautic_csv_export_test_');
$handle = fopen($tmpFile, 'r+');
$header = ['Submission ID', 'Contact ID', 'Form ID'];
try {
$putCsvExportRowRef = $this->getAccessibleReflectionMethod('putCsvExportRow');
$putCsvExportRowRef->invokeArgs($this->submissionModel, [$handle, $header]);
} catch (\ReflectionException $e) {
$this->fail($e->getMessage());
}
fclose($handle);
$result = array_map(fn ($line) => CsvHelper::strGetCsv($line), file($tmpFile));
$this->assertCount(1, $result);
$this->assertSame($header, $result[0]);
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
public function testGetExportRow(): void
{
$viewOnlyFields = ['button'];
$dateSubmitted = '28-03-2023 12:00';
$fixture = [
'id' => 1,
'leadId' => 123,
'dateSubmitted' => $dateSubmitted,
'ipAddress' => '127.0.0.1',
'referer' => 'https://test.com',
'results' => [
[
'type' => 'text',
'label' => 'Email',
'value' => 'a@b.c',
],
[
'type' => 'button',
'label' => 'Click',
'value' => true,
],
],
];
try {
$getExportRowRef = $this->getAccessibleReflectionMethod('getExportRow');
$result = $getExportRowRef->invokeArgs($this->submissionModel, [$fixture, $viewOnlyFields]);
} catch (\ReflectionException $e) {
$this->fail($e->getMessage());
}
$this->assertIsArray($result);
$this->assertSame([1, 123, $this->dateHelper->toFull($dateSubmitted, 'UTC'), '127.0.0.1', 'https://test.com', 'a@b.c'], $result);
}
public function testGetExportRowForPage(): void
{
$email = 'a@b.c';
$formId = 432;
$dateSubmitted = '28-03-2023 12:00';
$fixture = [
'id' => 1,
'leadId' => 123,
'dateSubmitted' => $dateSubmitted,
'ipAddress' => '127.0.0.1',
'referer' => 'https://test.com',
'formId' => $formId,
'results' => [
[
'type' => 'text',
'label' => 'Email',
'value' => $email,
],
],
];
try {
$getExportRowForPageRef = $this->getAccessibleReflectionMethod('getExportRowForPage');
$row1 = $getExportRowForPageRef->invokeArgs($this->submissionModel, [$fixture]);
$row2 = $getExportRowForPageRef->invokeArgs($this->submissionModel, [$fixture, 'xlsx']);
} catch (\ReflectionException $e) {
$this->fail($e->getMessage());
}
$this->assertIsArray($row1);
$this->assertIsArray($row2);
$this->assertCount(6, $row1);
$this->assertCount(5, $row2);
$this->assertSame([1, 123, $formId, $this->dateHelper->toFull($dateSubmitted, 'UTC'), '127.0.0.1', 'https://test.com'], $row1);
$this->assertSame([1, 123, $this->dateHelper->toFull($dateSubmitted, 'UTC'), '127.0.0.1', 'https://test.com'], $row2);
$this->assertNotContains($formId, $row2);
$this->assertNotContains($email, $row1);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Mautic\FormBundle\Tests\ProgressiveProfiling;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Entity\Form;
use Mautic\FormBundle\ProgressiveProfiling\DisplayCounter;
use Mautic\FormBundle\ProgressiveProfiling\DisplayManager;
class DisplayManagerTest extends \PHPUnit\Framework\TestCase
{
private Form $form;
private array $viewOnlyFields;
private DisplayCounter $displayCounter;
public function setUp(): void
{
$this->viewOnlyFields = [];
$this->form = new Form();
$this->displayCounter = new DisplayCounter($this->form);
}
public function testShowForField(): void
{
$form = new Form();
$viewOnlyFields = ['button'];
$displayManager = new DisplayManager($form, $viewOnlyFields);
$displayCounter = $displayManager->getDisplayCounter();
$field = new Field();
$this->assertTrue($displayManager->showForField($field));
$field->setType('button');
$this->assertTrue($displayManager->showForField($field));
$field->setType('text');
// display If first field is always display and progressive limit 1
$field->setAlwaysDisplay(true);
$form->setProgressiveProfilingLimit(1);
$this->assertTrue($displayManager->showForField($field));
// not display If second field is always display and progressive limit 1
$displayCounter->increaseDisplayedFields();
$this->assertFalse($displayManager->showForField($field));
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Mautic\FormBundle\Tests\Validator;
use Mautic\CoreBundle\Exception\FileInvalidException;
use Mautic\CoreBundle\Validator\FileUploadValidator;
use Mautic\FormBundle\Entity\Field;
use Mautic\FormBundle\Exception\FileValidationException;
use Mautic\FormBundle\Exception\NoFileGivenException;
use Mautic\FormBundle\Validator\UploadFieldValidator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\FileBag;
use Symfony\Component\HttpFoundation\Request;
#[\PHPUnit\Framework\Attributes\CoversClass(UploadFieldValidator::class)]
class UploadFieldValidatorTest extends \PHPUnit\Framework\TestCase
{
#[\PHPUnit\Framework\Attributes\TestDox('No Files given')]
public function testNoFilesGiven(): void
{
$fileUploadValidatorMock = $this->createMock(FileUploadValidator::class);
$fileUploadValidatorMock->expects($this->never())
->method('validate');
$parameterBagMock = $this->createMock(FileBag::class);
$parameterBagMock->expects($this->once())
->method('get')
->with('mauticform')
->willReturn(false);
$request = new Request();
$request->files = $parameterBagMock;
$fileUploadValidator = new UploadFieldValidator($fileUploadValidatorMock);
$field = new Field();
$this->expectException(NoFileGivenException::class);
$fileUploadValidator->processFileValidation($field, $request);
}
#[\PHPUnit\Framework\Attributes\TestDox('Exception should be thrown when validation fails')]
public function testValidationFailed(): void
{
$fileUploadValidatorMock = $this->createMock(FileUploadValidator::class);
$fileUploadValidatorMock->expects($this->once())
->method('validate')
->willThrowException(new FileInvalidException('Validation failed'));
$parameterBagMock = $this->createMock(FileBag::class);
$fileMock = $this->createMock(UploadedFile::class);
$files = [
'file' => $fileMock,
];
$parameterBagMock->expects($this->once())
->method('get')
->with('mauticform')
->willReturn($files);
$request = new Request();
$request->files = $parameterBagMock;
$fileUploadValidator = new UploadFieldValidator($fileUploadValidatorMock);
$field = new Field();
$field->setAlias('file');
$field->setProperties([
'allowed_file_size' => 1,
'allowed_file_extensions' => ['jpg', 'gif'],
]);
$this->expectException(FileValidationException::class);
$this->expectExceptionMessage('Validation failed');
$fileUploadValidator->processFileValidation($field, $request);
}
#[\PHPUnit\Framework\Attributes\TestDox('No validation error')]
public function testFileIsValid(): void
{
$fileUploadValidatorMock = $this->createMock(FileUploadValidator::class);
$fileUploadValidatorMock->expects($this->once())
->method('validate');
$parameterBagMock = $this->createMock(FileBag::class);
$fileMock = $this->createMock(UploadedFile::class);
$files = [
'file' => $fileMock,
];
$parameterBagMock->expects($this->once())
->method('get')
->with('mauticform')
->willReturn($files);
$request = new Request();
$request->files = $parameterBagMock;
$fileUploadValidator = new UploadFieldValidator($fileUploadValidatorMock);
$field = new Field();
$field->setAlias('file');
$field->setProperties([
'allowed_file_size' => 1,
'allowed_file_extensions' => ['jpg', 'gif'],
]);
$file = $fileUploadValidator->processFileValidation($field, $request);
$this->assertSame($fileMock, $file);
}
}