File: /home/xedaptot/ai.naniguide.com/tests/Unit/CampaignServerPoolTest.php
<?php
/**
* CampaignServerPoolTest — Covers Campaign::getServersPool() 3-branch logic
* for per-campaign sending-server narrow selection.
*
* A: pivot empty → default broadcast (MailList pool)
* B: pivot non-empty + active → RouletteWheel from pivot
* C: pivot non-empty + all dead → throw Exception (fail loud)
*/
use App\Library\RouletteWheel;
use App\Model\Campaign;
use App\Model\CampaignSendingServer;
use App\Model\Contact;
use App\Model\Customer;
use App\Model\Email;
use App\Model\MailList;
use App\Model\Plan;
use App\Model\SendingServer;
use App\Model\Subscription;
use App\Services\Plans\Entitlements\EntitlementKey;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
uses(TestCase::class);
afterEach(function () {
$testCustomerIds = DB::table('customers')->where('uid', 'like', 'pool_%')->pluck('id');
$campaignIds = DB::table('campaigns')->whereIn('customer_id', $testCustomerIds)->pluck('id');
DB::table('campaigns_sending_servers')->whereIn('campaign_id', $campaignIds)->delete();
DB::table('campaigns_lists_segments')->whereIn('campaign_id', $campaignIds)->delete();
DB::table('campaigns')->whereIn('id', $campaignIds)->delete();
DB::table('emails')->whereIn('customer_id', $testCustomerIds)->delete();
DB::table('mail_lists')->whereIn('customer_id', $testCustomerIds)->delete();
DB::table('sending_servers')->where('uid', 'like', 'poolsrv_%')->delete();
DB::table('subscriptions')->whereIn('customer_id', $testCustomerIds)->delete();
$planIds = DB::table('plans')->where('uid', 'like', 'poolplan_%')->pluck('id');
DB::table('plan_entitlements')->whereIn('plan_id', $planIds)->delete();
DB::table('plans')->whereIn('id', $planIds)->delete();
DB::table('customers')->whereIn('id', $testCustomerIds)->delete();
DB::table('contacts')->where('uid', 'like', 'poolcontact_%')->delete();
});
function poolFixture(int $serverCount = 2): array
{
$contact = Contact::forceCreate([
'uid' => uniqid('poolcontact_'),
'first_name' => 'P',
'last_name' => 'L',
'email' => uniqid() . '@pool-test.invalid',
]);
$customer = Customer::forceCreate([
'uid' => uniqid('pool_'),
'contact_id' => $contact->id,
'status' => 'active',
'timezone' => 'UTC',
]);
$plan = Plan::forceCreate([
'uid' => uniqid('poolplan_'),
'currency_id' => 1,
'name' => 'Pool Plan',
'price' => 0,
'frequency_amount' => 1,
'frequency_unit' => 'month',
'status' => 'active',
]);
DB::table('plan_entitlements')->insert([
'plan_id' => $plan->id,
'entitlement_key' => EntitlementKey::HAS_OWN_SENDING_SERVER->value,
'enabled' => true,
'created_at' => now(),
'updated_at' => now(),
]);
Subscription::forceCreate([
'uid' => uniqid('poolsub_'),
'customer_id' => $customer->id,
'plan_id' => $plan->id,
'status' => Subscription::STATUS_ACTIVE,
]);
$list = MailList::forceCreate([
'uid' => uniqid('poollist_'),
'name' => 'Pool List',
'customer_id' => $customer->id,
'from_email' => '[email protected]',
'from_name' => 'Pool Sender',
]);
// Customer's own active sending servers — `Customer::getSendingServerPool()`
// surfaces these directly when the HAS_OWN_SENDING_SERVER entitlement
// is granted (which the fixture above set up).
$servers = [];
for ($i = 0; $i < $serverCount; $i++) {
$servers[] = SendingServer::forceCreate([
'uid' => uniqid('poolsrv_'),
'name' => "Pool Server {$i}",
'type' => 'smtp',
'status' => SendingServer::STATUS_ACTIVE,
'host' => '127.0.0.1',
'smtp_port' => 25,
'quota_value' => -1,
'quota_base' => 1,
'quota_unit' => 'day',
'customer_id' => $customer->id,
]);
}
$campaign = $customer->local()->newDefaultCampaign();
$campaign->saveFromArray(['type' => Email::TYPE_REGULAR]);
$campaign->default_mail_list_id = $list->id;
$campaign->save();
return compact('customer', 'list', 'campaign', 'servers');
}
test('Nhánh A — pivot empty falls back to MailList default pool', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
expect($campaign->sendingServersPivot)->toHaveCount(0);
$pool = $campaign->getServersPool();
expect($pool)->toBeInstanceOf(RouletteWheel::class);
expect($pool->count())->toBe(2);
});
test('Nhánh B — pivot non-empty + active servers narrows to selection', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][0]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 70,
]);
$campaign->refresh();
$pool = $campaign->getServersPool();
expect($pool->count())->toBe(1);
$elements = $pool->getElements();
expect(reset($elements)->id)->toBe($fx['servers'][0]->id);
});
test('Nhánh B partial — pivot with mixed active/inactive yields only active', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
// Both selected, but server 1 marked inactive
$fx['servers'][1]->status = SendingServer::STATUS_INACTIVE;
$fx['servers'][1]->save();
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][0]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 50,
]);
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][1]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 50,
]);
$campaign->refresh();
$pool = $campaign->getServersPool();
expect($pool->count())->toBe(1);
});
test('Nhánh C — pivot non-empty with ALL inactive throws (fail loud, not silent fallback)', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
foreach ($fx['servers'] as $s) {
$s->status = SendingServer::STATUS_INACTIVE;
$s->save();
}
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][0]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 100,
]);
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][1]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 100,
]);
$campaign->refresh();
expect(fn () => $campaign->getServersPool())->toThrow(\Exception::class, 'all per-campaign sending servers are inactive');
});
test('FK cascade — deleting sending_server clears pivot, drops into nhánh A', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
CampaignSendingServer::forceCreate([
'campaign_id' => $campaign->id,
'sending_server_id' => $fx['servers'][0]->id,
'customer_id' => $fx['customer']->id,
'fitness' => 100,
]);
// Delete the picked server — FK cascade drops the pivot row
$fx['servers'][0]->delete();
$campaign->refresh();
expect($campaign->sendingServersPivot)->toHaveCount(0);
// Now nhánh A: falls back to MailList pool (still has server[1])
$pool = $campaign->getServersPool();
expect($pool)->toBeInstanceOf(RouletteWheel::class);
expect($pool->count())->toBe(1);
});
test('availableSendingServerIds returns the eligible pool from defaultMailList', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
$ids = $campaign->availableSendingServerIds();
expect($ids)->toHaveCount(2);
expect($ids)->toContain($fx['servers'][0]->id);
expect($ids)->toContain($fx['servers'][1]->id);
});
test('syncSendingServers replaces pivot rows on each call', function () {
$fx = poolFixture(serverCount: 2);
$campaign = $fx['campaign'];
$campaign->syncSendingServers([
['sending_server_id' => $fx['servers'][0]->id, 'fitness' => 30],
['sending_server_id' => $fx['servers'][1]->id, 'fitness' => 70],
]);
expect($campaign->sendingServersPivot()->count())->toBe(2);
// Re-sync with just one entry → previous rows wiped
$campaign->syncSendingServers([
['sending_server_id' => $fx['servers'][0]->id, 'fitness' => 100],
]);
expect($campaign->sendingServersPivot()->count())->toBe(1);
// Empty array clears all → drops into nhánh A
$campaign->syncSendingServers([]);
expect($campaign->sendingServersPivot()->count())->toBe(0);
});