File: /home/xedaptot/be.naniguide.com/tests/Unit/Sending/Dispatch/CampaignDeliveryDispatcherTest.php
<?php
/**
* CampaignDeliveryDispatcherTest — verifies the per-page job builder constructs
* one SendMessage per subscriber, propagates stopOnError + variant, and feeds
* each built job to the caller-supplied closure.
*
* Mocks Campaign + Subscriber + RouletteWheel + Subscription via Mockery so the
* dispatcher logic is testable without DB factories. The query chain
* `subscribersToSend()->whereIn(...)->get()` is stubbed to return an Eloquent
* Collection of fake Subscribers.
*/
use App\Jobs\SendMessage;
use App\Library\RouletteWheel;
use App\Library\Sending\Dispatch\CampaignDeliveryDispatcher;
use App\Model\AbTestVariant;
use App\Model\Campaign;
use App\Model\Customer;
use App\Model\Subscriber;
use App\Model\Subscription;
use Illuminate\Database\Eloquent\Collection;
use Tests\TestCase;
uses(TestCase::class);
function makeDispatcherCampaign(array $subscribers, bool $stopOnError = false, ?Subscription $subscription = null): Campaign
{
$logger = Mockery::mock();
$logger->shouldReceive('info')->byDefault();
$serverPool = Mockery::mock(RouletteWheel::class);
$customer = Mockery::mock(Customer::class);
$customer->shouldReceive('getCurrentActiveSubscription')->andReturn($subscription)->byDefault();
$whereInBuilder = Mockery::mock();
$whereInBuilder->shouldReceive('get')->andReturn(new Collection($subscribers));
$subscribersBuilder = Mockery::mock();
$subscribersBuilder->shouldReceive('whereIn')->with('subscribers.id', Mockery::any())->andReturn($whereInBuilder);
$campaign = Mockery::mock(Campaign::class);
$campaign->shouldReceive('subscribersToSend')->andReturn($subscribersBuilder);
$campaign->shouldReceive('getServersPool')->andReturn($serverPool);
$campaign->shouldReceive('logger')->andReturn($logger);
$campaign->shouldReceive('stopOnError')->andReturn($stopOnError);
$campaign->shouldReceive('getAttribute')->with('customer')->andReturn($customer);
return $campaign;
}
function makeFakeSubscriber(int $id): Subscriber
{
$subscriber = Mockery::mock(Subscriber::class);
$subscriber->shouldReceive('getAttribute')->with('id')->andReturn($id);
return $subscriber;
}
it('builds one SendMessage per subscriber and feeds each to the closure', function () {
$subs = [makeFakeSubscriber(1), makeFakeSubscriber(2), makeFakeSubscriber(3)];
$campaign = makeDispatcherCampaign($subs);
$built = [];
(new CampaignDeliveryDispatcher())->buildDeliveryJobs(
$campaign,
page: 1,
subscriberIds: [1, 2, 3],
variant: null,
onJob: function (SendMessage $job) use (&$built) {
$built[] = $job;
},
);
expect($built)->toHaveCount(3);
foreach ($built as $job) {
expect($job)->toBeInstanceOf(SendMessage::class);
}
});
it('passes stopOnError flag from the campaign onto each job', function () {
$campaign = makeDispatcherCampaign([makeFakeSubscriber(1)], stopOnError: true);
$captured = null;
(new CampaignDeliveryDispatcher())->buildDeliveryJobs(
$campaign,
1,
[1],
null,
function (SendMessage $job) use (&$captured) {
$captured = $job;
},
);
// Inspect the protected $stopOnError via Reflection — it's set via setStopOnError(true).
$ref = new ReflectionClass(SendMessage::class);
$prop = $ref->getProperty('stopOnError');
$prop->setAccessible(true);
expect($prop->getValue($captured))->toBeTrue();
});
it('passes the variant through to the SendMessage constructor', function () {
$variant = Mockery::mock(AbTestVariant::class);
$campaign = makeDispatcherCampaign([makeFakeSubscriber(1)]);
$captured = null;
(new CampaignDeliveryDispatcher())->buildDeliveryJobs(
$campaign,
1,
[1],
$variant,
function (SendMessage $job) use (&$captured) {
$captured = $job;
},
);
$ref = new ReflectionClass(SendMessage::class);
$prop = $ref->getProperty('variant');
$prop->setAccessible(true);
expect($prop->getValue($captured))->toBe($variant);
});
it('logs "no contacts" when the page resolves to zero subscribers', function () {
$logger = Mockery::mock();
$logger->shouldReceive('info')->withArgs(function ($msg) {
return str_contains($msg, 'no contacts in this page');
})->once();
$whereInBuilder = Mockery::mock();
$whereInBuilder->shouldReceive('get')->andReturn(new Collection([]));
$subscribersBuilder = Mockery::mock();
$subscribersBuilder->shouldReceive('whereIn')->andReturn($whereInBuilder);
$campaign = Mockery::mock(Campaign::class);
$campaign->shouldReceive('subscribersToSend')->andReturn($subscribersBuilder);
$campaign->shouldReceive('getServersPool')->andReturn(Mockery::mock(RouletteWheel::class));
$campaign->shouldReceive('logger')->andReturn($logger);
$campaign->shouldReceive('stopOnError')->andReturn(false);
$called = 0;
(new CampaignDeliveryDispatcher())->buildDeliveryJobs(
$campaign,
7,
[99, 100],
null,
function () use (&$called) { $called++; },
);
expect($called)->toBe(0);
});
it('logs the first/last subscriber id range for non-empty pages', function () {
$subs = [makeFakeSubscriber(101), makeFakeSubscriber(102), makeFakeSubscriber(103)];
$logger = Mockery::mock();
$logger->shouldReceive('info')->withArgs(function ($msg) {
return str_contains($msg, 'Page 5')
&& str_contains($msg, 'from guy 101')
&& str_contains($msg, 'to 103');
})->once();
$whereInBuilder = Mockery::mock();
$whereInBuilder->shouldReceive('get')->andReturn(new Collection($subs));
$subscribersBuilder = Mockery::mock();
$subscribersBuilder->shouldReceive('whereIn')->andReturn($whereInBuilder);
$customer = Mockery::mock(Customer::class);
$customer->shouldReceive('getCurrentActiveSubscription')->andReturn(null);
$campaign = Mockery::mock(Campaign::class);
$campaign->shouldReceive('subscribersToSend')->andReturn($subscribersBuilder);
$campaign->shouldReceive('getServersPool')->andReturn(Mockery::mock(RouletteWheel::class));
$campaign->shouldReceive('logger')->andReturn($logger);
$campaign->shouldReceive('stopOnError')->andReturn(false);
$campaign->shouldReceive('getAttribute')->with('customer')->andReturn($customer);
(new CampaignDeliveryDispatcher())->buildDeliveryJobs(
$campaign,
5,
[101, 102, 103],
null,
fn () => null,
);
});