segmentCountCacheHelper = static::getContainer()->get('mautic.helper.segment.count.cache'); } public function beforeTearDown(): void { parent::beforeTearDown(); putenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS=0'); $this->segmentCountCacheHelper = null; } /** * @throws \Exception */ public function testCampaignExecutionForAll(): void { // Process in batches of 10 to ensure batching is working as expected $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Let's analyze $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]); $tags = $this->getTagCounts(); // Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1 $this->assertCount(50, $byEvent[1]); $this->assertCount(50, $byEvent[2]); // Sending Campaign Test Email 1 should be scheduled foreach ($byEvent[2] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']); } } // Everyone should have had the Is US condition processed $this->assertCount(50, $byEvent[11]); // 42 should have been send down the non-action path (red) of the condition $nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]); $this->assertEquals(42, $nonActionCount); // 8 contacts are from the US and should be labeled with US:Action $this->assertCount(8, $byEvent[12]); $this->assertEquals(8, $tags['US:Action']); // Those tagged with US:Action should also be tagged with ChainedAction by a chained event. $this->assertCount(8, $byEvent[16]); $this->assertEquals(8, $tags['ChainedAction']); // The rest (42) contacts are not from the US and should be labeled with NonUS:Action $this->assertCount(42, $byEvent[13]); $this->assertEquals(42, $tags['NonUS:Action']); // No emails should be sent till after 5 seconds and the command is ran again $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id <= 25') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(0, $stats); // Wait 6 seconds then execute the campaign again to send scheduled events static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds')); $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Send email 1 should no longer be scheduled $byEvent = $this->getCampaignEventLogs([2, 4]); $this->assertCount(50, $byEvent[2]); foreach ($byEvent[2] as $log) { if (1 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']); } } // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); // Check that the emails actually sent $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id <= 25') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(25, $stats); // Now let's simulate email opens foreach ($stats as $stat) { $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true)); } $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); $this->assertCount(0, $byEvent[15]); // Those 25 should now have open email decisions logged and the next email sent $this->assertCount(25, $byEvent[3]); $this->assertCount(25, $byEvent[10]); // Wait another 6 seconds to go beyond the inaction timeframe static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds')); // Execute the command again to trigger inaction related events $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '-l' => 10]); // Now we should have 50 email open decisions $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); $this->assertCount(50, $byEvent[3]); // 25 should be marked as non_action_path_taken $nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]); $this->assertEquals(25, $nonActionCount); // A condition should be logged as evaluated for each of the 25 contacts $this->assertCount(25, $byEvent[4]); $this->assertCount(25, $byEvent[5]); // Tag EmailNotOpen should all be scheduled for these 25 contacts because the condition's timeframe was shorter and therefore the // contact was sent down the inaction path $this->assertCount(25, $byEvent[14]); $this->assertCount(25, $byEvent[15]); $utcTimezone = new \DateTimeZone('UTC'); foreach ($byEvent[14] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (2 !== $diff->i) { $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); } } foreach ($byEvent[15] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (6 !== $diff->i) { $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); } } $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); $tags = $this->getTagCounts(); // Of those that did not open the email, 6 should be tagged US:NotOpen $this->assertCount(6, $byEvent[6]); $this->assertEquals(6, $tags['US:NotOpen']); // And 19 should be tagged NonUS:NotOpen $this->assertCount(19, $byEvent[7]); $this->assertEquals(19, $tags['NonUS:NotOpen']); // And 4 should be tagged UK:NotOpen $this->assertCount(4, $byEvent[8]); $this->assertEquals(4, $tags['UK:NotOpen']); // And 21 should be tagged NonUK:NotOpen $this->assertCount(21, $byEvent[9]); $this->assertEquals(21, $tags['NonUK:NotOpen']); // No one should be tagged as EmailNotOpen because the actions are still scheduled $this->assertFalse(isset($tags['EmailNotOpen'])); } /** * @throws \Exception */ public function testCampaignExecutionForOne(): void { $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // Let's analyze $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]); $tags = $this->getTagCounts(); // Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1 $this->assertCount(1, $byEvent[1]); $this->assertCount(1, $byEvent[2]); // Sending Campaign Test Email 1 should be scheduled foreach ($byEvent[2] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']); } } // Everyone should have had the Is US condition processed $this->assertCount(1, $byEvent[11]); // 1 should have been send down the non-action path (red) of the condition $nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]); $this->assertEquals(1, $nonActionCount); // 0 contacts are from the US and should be labeled with US:Action $this->assertCount(0, $byEvent[12]); $this->assertTrue(empty($tags['US:Action'])); // None tagged with US:Action, so none should be tagged with ChainedAction by a chained event. $this->assertCount(0, $byEvent[16]); $this->assertTrue(empty($tags['ChainedAction'])); // The rest (1) contacts are not from the US and should be labeled with NonUS:Action $this->assertCount(1, $byEvent[13]); $this->assertEquals(1, $tags['NonUS:Action']); // No emails should be sent till after 5 seconds and the command is ran again $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id = 1') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(0, $stats); // Wait 6 seconds then execute the campaign again to send scheduled events static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds')); $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // Send email 1 should no longer be scheduled $byEvent = $this->getCampaignEventLogs([2, 4]); $this->assertCount(1, $byEvent[2]); foreach ($byEvent[2] as $log) { if (1 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']); } } // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); // Check that the emails actually sent $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id = 1') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(1, $stats); // Now let's simulate email opens foreach ($stats as $stat) { $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true)); } $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); $this->assertCount(0, $byEvent[15]); // The 1 should now have open email decisions logged and the next email sent $this->assertCount(1, $byEvent[3]); $this->assertCount(1, $byEvent[10]); // Wait 6 seconds to go beyond the inaction timeframe static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds')); // Execute the command again to trigger inaction related events $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-id' => 1]); // Now we should have 1 email open decisions $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); $this->assertCount(1, $byEvent[3]); // 0 should be marked as non_action_path_taken $nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]); $this->assertEquals(0, $nonActionCount); // There should be no inactive events $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); $this->assertCount(0, $byEvent[15]); $utcTimezone = new \DateTimeZone('UTC'); foreach ($byEvent[14] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (2 !== $diff->i) { $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); } } foreach ($byEvent[15] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (6 !== $diff->i) { $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); } } $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); $tags = $this->getTagCounts(); // Of those that did not open the email, 0 should be tagged US:NotOpen $this->assertCount(0, $byEvent[6]); $this->assertTrue(empty($tags['US:NotOpen'])); // And 0 should be tagged NonUS:NotOpen $this->assertCount(0, $byEvent[7]); $this->assertTrue(empty($tags['NonUS:NotOpen'])); // And 0 should be tagged UK:NotOpen $this->assertCount(0, $byEvent[8]); $this->assertTrue(empty($tags['UK:NotOpen'])); // And 0 should be tagged NonUK:NotOpen $this->assertCount(0, $byEvent[9]); $this->assertTrue(empty($tags['NonUK:NotOpen'])); // No one should be tagged as EmailNotOpen because the actions are still scheduled $this->assertTrue(empty($tags['EmailNotOpen'])); } public function testCampaignExecutionForSome(): void { $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); // Let's analyze $byEvent = $this->getCampaignEventLogs([1, 2, 11, 12, 13, 16]); $tags = $this->getTagCounts(); // Everyone should have been tagged with CampaignTest and have been sent Campaign Test Email 1 $this->assertCount(5, $byEvent[1]); $this->assertCount(5, $byEvent[2]); // Sending Campaign Test Email 1 should be scheduled foreach ($byEvent[2] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 was not scheduled for lead ID '.$log['lead_id']); } } // Everyone should have had the Is US condition processed $this->assertCount(5, $byEvent[11]); // 4 should have been send down the non-action path (red) of the condition $nonActionCount = $this->getNonActionPathTakenCount($byEvent[11]); $this->assertEquals(4, $nonActionCount); // 1 contacts are from the US and should be labeled with US:Action $this->assertCount(1, $byEvent[12]); $this->assertEquals(1, $tags['US:Action']); // Those tagged with US:Action should also be tagged with ChainedAction by a chained event. $this->assertCount(1, $byEvent[16]); $this->assertEquals(1, $tags['ChainedAction']); // The rest (4) contacts are not from the US and should be labeled with NonUS:Action $this->assertCount(4, $byEvent[13]); $this->assertEquals(4, $tags['NonUS:Action']); // No emails should be sent till after 5 seconds and the command is ran again $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id <= 2') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(0, $stats); // Wait 6 seconds then execute the campaign again to send scheduled events static::getContainer()->get(ScheduledExecutioner::class)->setNowTime(new \DateTime('+'.self::CONDITION_SECONDS.' seconds')); $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); // Send email 1 should no longer be scheduled $byEvent = $this->getCampaignEventLogs([2, 4]); $this->assertCount(5, $byEvent[2]); foreach ($byEvent[2] as $log) { if (1 === (int) $log['is_scheduled']) { $this->fail('Sending Campaign Test Email 1 is still scheduled for lead ID '.$log['lead_id']); } } // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); // Check that the emails actually sent $stats = $this->db->createQueryBuilder() ->select('*') ->from($this->prefix.'email_stats', 'stat') ->where('stat.lead_id <= 2') ->executeQuery() ->fetchAllAssociative(); $this->assertCount(2, $stats); // Now let's simulate email opens foreach ($stats as $stat) { $this->client->request('GET', '/email/'.$stat['tracking_hash'].'.gif'); $this->assertEquals(200, $this->client->getResponse()->getStatusCode(), var_export($this->client->getResponse()->getContent(), true)); } $byEvent = $this->getCampaignEventLogs([3, 4, 5, 10, 14, 15]); // The non-action events attached to the decision should have no logs entries $this->assertCount(0, $byEvent[4]); $this->assertCount(0, $byEvent[5]); $this->assertCount(0, $byEvent[14]); $this->assertCount(0, $byEvent[15]); // Those 25 should now have open email decisions logged and the next email sent $this->assertCount(2, $byEvent[3]); $this->assertCount(2, $byEvent[10]); // Wait 6 seconds to go beyond the inaction timeframe static::getContainer()->get(InactiveExecutioner::class)->setNowTime(new \DateTime('+'.(self::CONDITION_SECONDS * 2).' seconds')); // Execute the command again to trigger inaction related events $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1, '--contact-ids' => '1,2,3,4,19']); // Now we should have 5 email open decisions $byEvent = $this->getCampaignEventLogs([3, 4, 5, 14, 15]); $this->assertCount(5, $byEvent[3]); // 3 should be marked as non_action_path_taken $nonActionCount = $this->getNonActionPathTakenCount($byEvent[3]); $this->assertEquals(3, $nonActionCount); // A condition should be logged as evaluated for each of the 3 contacts $this->assertCount(3, $byEvent[4]); $this->assertCount(3, $byEvent[5]); // Tag EmailNotOpen should all be scheduled for these 3 contacts because the condition's timeframe was shorter and therefore the // contact was sent down the inaction path $this->assertCount(3, $byEvent[14]); $this->assertCount(3, $byEvent[15]); $utcTimezone = new \DateTimeZone('UTC'); foreach ($byEvent[14] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (2 !== $diff->i) { $this->fail('Tag EmailNotOpen should be scheduled for around 2 minutes ('.$diff->i.' minutes)'); } } foreach ($byEvent[15] as $log) { if (0 === (int) $log['is_scheduled']) { $this->fail('Tag EmailNotOpen Again is not scheduled for lead ID '.$log['lead_id']); } $scheduledFor = new \DateTime($log['trigger_date'], $utcTimezone); $diff = $this->eventDate->diff($scheduledFor); if (6 !== $diff->i) { $this->fail('Tag EmailNotOpen Again should be scheduled for around 6 minutes ('.$diff->i.' minutes)'); } } $byEvent = $this->getCampaignEventLogs([6, 7, 8, 9]); $tags = $this->getTagCounts(); // Of those that did not open the email, 1 should be tagged US:NotOpen $this->assertCount(1, $byEvent[6]); $this->assertEquals(1, $tags['US:NotOpen']); // And 2 should be tagged NonUS:NotOpen $this->assertCount(2, $byEvent[7]); $this->assertEquals(2, $tags['NonUS:NotOpen']); // And 2 should be tagged UK:NotOpen $this->assertCount(2, $byEvent[8]); $this->assertEquals(2, $tags['UK:NotOpen']); // And 1 should be tagged NonUK:NotOpen $this->assertCount(1, $byEvent[9]); $this->assertEquals(1, $tags['NonUK:NotOpen']); // No one should be tagged as EmailNotOpen because the actions are still scheduled $this->assertFalse(isset($tags['EmailNotOpen'])); } public function testCampaignActionChangeMembership(): void { $campaign1 = $this->createCampaign('Campaign 1'); $campaign2 = $this->createCampaign('Campaign 2'); $lead = $this->createLead('Lead'); $this->createCampaignLead($campaign1, $lead); $this->createCampaignLead($campaign2, $lead); $this->em->flush(); $property = ['addTo' => [$campaign2->getId()], 'removeFrom' => ['this']]; $this->createEvent('Event', $campaign1, 'campaign.addremovelead', 'action', $property); $this->em->flush(); $this->em->clear(); $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign1->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]); $campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead], ['campaign' => 'ASC']); Assert::assertCount(2, $campaignLeads); Assert::assertSame($campaign1->getId(), $campaignLeads[0]->getCampaign()->getId()); Assert::assertTrue($campaignLeads[0]->getManuallyRemoved()); Assert::assertSame($campaign2->getId(), $campaignLeads[1]->getCampaign()->getId()); Assert::assertFalse($campaignLeads[1]->getManuallyRemoved()); } public function testCampaignActionAfterChangeMembership(): void { $campaign = $this->createCampaign('Campaign 1'); $lead = $this->createLead('Lead'); $this->createCampaignLead($campaign, $lead); $this->em->flush(); $property = ['removeFrom' => ['this']]; $event1 = $this->createEvent('Event', $campaign, 'campaign.addremovelead', 'action', $property); $property = ['points' => 1]; $event2 = $this->createEvent('Event', $campaign, 'lead.changepoints', 'action', $property); $this->em->flush(); $this->em->clear(); $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]); $campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead]); Assert::assertCount(1, $campaignLeads); Assert::assertSame($campaign->getId(), $campaignLeads[0]->getCampaign()->getId()); Assert::assertTrue($campaignLeads[0]->getManuallyRemoved()); $campaignEventLogs = $this->em->getRepository(LeadEventLog::class)->findBy(['campaign' => $campaign, 'lead' => $lead], ['event' => 'ASC']); Assert::assertCount(1, $campaignEventLogs); Assert::assertSame($campaign->getId(), $campaignEventLogs[0]->getCampaign()->getId()); Assert::assertSame($event1->getId(), $campaignEventLogs[0]->getEvent()->getId()); } public function testCampaignActionBeforeChangeMembership(): void { $campaign = $this->createCampaign('Campaign 1'); $lead = $this->createLead('Lead'); $this->createCampaignLead($campaign, $lead); $this->em->flush(); $property = ['points' => 1]; $event1 = $this->createEvent('Event', $campaign, 'lead.changepoints', 'action', $property); $property = ['removeFrom' => ['this']]; $event2 = $this->createEvent('Event', $campaign, 'campaign.addremovelead', 'action', $property); $this->em->flush(); $this->em->clear(); $this->testSymfonyCommand('mautic:campaigns:trigger', ['--campaign-id' => $campaign->getId(), '--contact-id' => $lead->getId(), '--kickoff-only' => true]); $campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead]); Assert::assertCount(1, $campaignLeads); Assert::assertSame($campaign->getId(), $campaignLeads[0]->getCampaign()->getId()); Assert::assertTrue($campaignLeads[0]->getManuallyRemoved()); $campaignEventLogs = $this->em->getRepository(LeadEventLog::class)->findBy(['campaign' => $campaign, 'lead' => $lead], ['event' => 'ASC']); Assert::assertCount(2, $campaignEventLogs); Assert::assertSame($campaign->getId(), $campaignEventLogs[0]->getCampaign()->getId()); Assert::assertSame($event1->getId(), $campaignEventLogs[0]->getEvent()->getId()); Assert::assertSame($campaign->getId(), $campaignEventLogs[1]->getCampaign()->getId()); Assert::assertSame($event2->getId(), $campaignEventLogs[1]->getEvent()->getId()); } public function testCampaignExclusion(): void { $campaign1 = $this->createCampaign('Campaign 1'); $campaign2 = $this->createCampaign('Campaign 2'); $lead = $this->createLead('Lead'); $this->createCampaignLead($campaign1, $lead); $this->createCampaignLead($campaign2, $lead); $this->em->flush(); $property = ['addTo' => [$campaign2->getId()], 'removeFrom' => ['this']]; $this->createEvent('Event', $campaign1, 'campaign.addremovelead', 'action', $property); $this->em->flush(); $this->em->clear(); $this->testSymfonyCommand('mautic:campaigns:trigger', ['--exclude' => [$campaign1->getId()], '--contact-id' => $lead->getId(), '--kickoff-only' => true]); $campaignLeads = $this->em->getRepository(Lead::class)->findBy(['lead' => $lead], ['campaign' => 'ASC']); Assert::assertCount(2, $campaignLeads); Assert::assertSame($campaign1->getId(), $campaignLeads[0]->getCampaign()->getId()); Assert::assertFalse($campaignLeads[0]->getManuallyRemoved(), 'Test not executed campaign does not have Contact removed.'); Assert::assertSame($campaign2->getId(), $campaignLeads[1]->getCampaign()->getId()); Assert::assertFalse($campaignLeads[1]->getManuallyRemoved()); Assert::assertFalse($campaignLeads[1]->getManuallyAdded()); } /** * @see https://github.com/mautic/mautic/issues/11061 * * This test will not fail if the infinite loop returns and instead run indefinitelly until a PHPUNIT timeout is reached. * I couldn't find an easy way to test for an infinite loop. But we'll know if it returns again. * We'll just spend more time figuring out which test is taking so long. */ public function testCampaignInfiniteLoop(): void { $campaignMemberRepo = $this->em->getRepository(Lead::class); $segmentMemberRepo = $this->em->getRepository(ListLead::class); $campaignRepo = $this->em->getRepository(Campaign::class); // Clear the campaign and segment members as those are manually_added. $campaignMemberRepo->deleteEntities($campaignMemberRepo->findAll()); $segmentMemberRepo->deleteEntities($segmentMemberRepo->findAll()); $campaign = $campaignRepo->find(1); // Created in parent::setUp() \assert($campaign instanceof Campaign); $campaign->setAllowRestart(true); $campaignRepo->saveEntity($campaign); $john = $this->createLead('John'); $jane = $this->createLead('Jane'); $this->createSegmentMember($campaign->getLists()->first(), $john); $this->createSegmentMember($campaign->getLists()->first(), $jane); $this->createCampaignLead($campaign, $john); $this->createCampaignLead($campaign, $jane, true); // Manually removed. $this->em->flush(); $this->em->detach($campaign); $this->em->detach($campaignRepo); $tStart = microtime(true); $this->testSymfonyCommand('mautic:campaigns:update', ['--campaign-id' => $campaign->getId()]); $tDiff = microtime(true) - $tStart; $this->assertLessThan(10, $tDiff, 'The campaign rebuild takes more than 10 seconds, probably an infinite loop.'); } public function testCampaignExecuteOrderByDateCreatedDesc(): void { $oldCampaign = $this->createCampaign('Some old campaign'); $oldCampaign->setDateAdded(new \DateTime('2019-01-03 03:54:25')); $newCampaign = $this->createCampaign('New campaign'); $newCampaign->setDateAdded(new \DateTime()); $this->em->flush(); $commandTester = $this->testSymfonyCommand('mautic:campaigns:trigger'); $commandTester->assertCommandIsSuccessful(); $lines = preg_split('/\r\n|\r|\n/', $commandTester->getDisplay()); $campaignStartLines = []; foreach ($lines as $line) { if (str_starts_with($line, 'Triggering events for campaign')) { $campaignStartLines[] = $line; } } // check if the new campaign processed first $this->assertEquals("Triggering events for campaign {$newCampaign->getId()}", $campaignStartLines[0]); } /** * @throws \Exception */ public function testSegmentCacheCount(): void { // Execute the command again to trigger related events. $this->testSymfonyCommand('mautic:campaigns:trigger', ['-i' => 1]); // Segment cache count should be 50. $count = $this->segmentCountCacheHelper->getSegmentContactCount(1); self::assertEquals(50, $count); } /** * @return array */ private function getTagCounts() { $tags = $this->db->createQueryBuilder() ->select('t.tag, count(*) as the_count') ->from($this->prefix.'lead_tags', 't') ->join('t', $this->prefix.'lead_tags_xref', 'l', 't.id = l.tag_id') ->groupBy('t.tag') ->executeQuery() ->fetchAllAssociative(); $tagCounts = []; foreach ($tags as $tag) { $tagCounts[$tag['tag']] = (int) $tag['the_count']; } return $tagCounts; } /** * @return int */ private function getNonActionPathTakenCount(array $logs) { $nonActionCount = 0; foreach ($logs as $log) { if ((int) $log['non_action_path_taken']) { ++$nonActionCount; } } return $nonActionCount; } }