HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/tests/Feature/SendingServerWebhooks/RecordBounceListenerTest.php
<?php

use App\Listeners\SendingServerWebhooks\RecordBounce;
use App\Model\Blacklist;
use App\Model\BounceLog;
use App\Model\Contact;
use App\Model\Customer;
use App\Model\MailList;
use App\Model\SendingServer;
use App\Model\Subscriber;
use App\Model\TrackingLog;
use App\SendingServers\Webhooks\BounceReceived;
use App\SendingServers\Webhooks\BounceType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;

uses(TestCase::class, DatabaseTransactions::class);

/**
 * Listener integration tests: BounceReceived → BounceLog row written + blacklist
 * applied when subscriber found. Exercises the FULL DB write path that the
 * legacy DeliveryController vendor handlers used to perform.
 *
 * Fixture pattern: messageId formatted as `<random>.<customerUid>@<host>` so
 * `Customer::getCustomerFromKey` resolves via `extractCustomerUidFromMessageId`
 * (regex `/\.(?<CustomerUid>[a-z0-9]+)@/`).
 */

function makeBounceFixture(): array
{
    $contact = Contact::factory()->create();
    $customer = Customer::factory()->create([
        'contact_id' => $contact->id,
        'uid'        => 'rblstcust' . uniqid(),
    ]);
    $list = MailList::forceCreate([
        'uid'         => 'rblst_' . uniqid(),
        'customer_id' => $customer->id,
        'name'        => 'fixture',
        'from_name'   => 'Fixture',
        'from_email'  => '[email protected]',
    ]);
    $subscriber = Subscriber::factory()->create([
        'mail_list_id' => $list->id,
        'email'        => '[email protected]',
    ]);
    $msgId = 'fix' . uniqid() . '.' . $customer->uid . '@example.com';
    $server = SendingServer::factory()->create();
    $trackingLog = TrackingLog::forceCreate([
        'runtime_message_id' => $msgId,
        'message_id'         => 'vendor-msg-id-fixture',
        'customer_id'        => $customer->id,
        'subscriber_id'      => $subscriber->id,
        'sending_server_id'  => $server->id,
        'status'             => 'sent',
    ]);

    return compact('customer', 'list', 'subscriber', 'msgId', 'trackingLog', 'server');
}

test('BounceReceived → BounceLog row with vendor-supplied raw bounce_type', function () {
    $f = makeBounceFixture();

    $event = new BounceReceived(
        runtimeMessageId: $f['msgId'],
        type: BounceType::HARD,
        bounceTypeRaw: BounceLog::HARD,
        shouldBlacklist: false,           // skip blacklist path for this test
        rawForDb: '{"raw":"payload"}',
        rawPayload: ['parsed' => 'payload'],
        server: $f['server'],
    );

    (new RecordBounce())->handle($event);

    $log = BounceLog::where('runtime_message_id', $f['msgId'])->firstOrFail();
    expect($log->bounce_type)->toBe(BounceLog::HARD)
        ->and($log->raw)->toBe('{"raw":"payload"}')
        ->and($log->tracking_log_id)->toBe($f['trackingLog']->id)
        ->and($log->message_id)->toBe('vendor-msg-id-fixture');     // from tracking log
});

test('BounceReceived with shouldBlacklist=true → subscriber blacklisted', function () {
    $f = makeBounceFixture();

    $event = new BounceReceived(
        runtimeMessageId: $f['msgId'],
        type: BounceType::HARD,
        bounceTypeRaw: BounceLog::HARD,
        shouldBlacklist: true,
        rawForDb: '{"raw":"payload"}',
        rawPayload: [],
        server: $f['server'],
    );

    (new RecordBounce())->handle($event);

    $blacklisted = Blacklist::where('email', $f['subscriber']->email)->exists();
    expect($blacklisted)->toBeTrue();
});

test('BounceReceived with messageIdAlwaysRuntime=true overrides tracking_log message_id (Mailgun parity)', function () {
    $f = makeBounceFixture();

    $event = new BounceReceived(
        runtimeMessageId: $f['msgId'],
        type: BounceType::HARD,
        bounceTypeRaw: BounceLog::HARD,
        shouldBlacklist: false,
        rawForDb: '{}',
        rawPayload: [],
        server: $f['server'],
        messageIdAlwaysRuntime: true,
    );

    (new RecordBounce())->handle($event);

    $log = BounceLog::where('runtime_message_id', $f['msgId'])->firstOrFail();
    // PARITY: legacy SendingServerMailgun set message_id := runtime_message_id,
    // OVERRIDING the tracking-log's message_id.
    expect($log->message_id)->toBe($f['msgId']);
});

test('BounceReceived with directBlacklistEmail (AWS Q4) → direct Blacklist row when no subscriber match', function () {
    // No subscriber linked to this msgId — Q4 path: AWS knows the email
    // from SNS payload and writes directly to Blacklist.
    $contact = Contact::factory()->create();
    $customer = Customer::factory()->create(['uid' => 'awscust' . uniqid(), 'contact_id' => $contact->id]);
    $msgId = 'aws-msg-' . uniqid() . '.' . $customer->uid . '@aws.example.com';
    // Note: NO TrackingLog created (Q4 happens when tracking log is missing).

    $server = new SendingServer(['type' => 'amazon-api', 'name' => 'fixture']);
    $server->uid = 'srv-aws-fixture';

    $event = new BounceReceived(
        runtimeMessageId: $msgId,
        type: BounceType::HARD,
        bounceTypeRaw: 'Permanent',           // PARITY: AWS raw string, not BounceLog::HARD
        shouldBlacklist: true,
        rawForDb: '{"sns":"payload"}',
        rawPayload: [],
        server: $server,
        directBlacklistEmail: '[email protected]',
    );

    (new RecordBounce())->handle($event);

    expect(BounceLog::where('runtime_message_id', $msgId)->exists())->toBeTrue();
    expect(Blacklist::where('email', '[email protected]')->exists())->toBeTrue();
});

test('BounceReceived for unknown customer → log + skip (no DB write)', function () {
    $server = new SendingServer(['type' => 'sendgrid-api', 'name' => 'fixture']);
    $server->uid = 'srv-unknown';

    $event = new BounceReceived(
        runtimeMessageId: '[email protected]',
        type: BounceType::HARD,
        bounceTypeRaw: BounceLog::HARD,
        shouldBlacklist: true,
        rawForDb: '{}',
        rawPayload: [],
        server: $server,
    );

    (new RecordBounce())->handle($event);

    expect(BounceLog::where('runtime_message_id', '[email protected]')->exists())->toBeFalse();
});

test('BounceReceived with useLegacyRecordHardBounce=true delegates to BounceLog::recordHardBounce (Blastengine parity)', function () {
    $f = makeBounceFixture();

    $event = new BounceReceived(
        runtimeMessageId: $f['msgId'],
        type: BounceType::HARD,
        bounceTypeRaw: BounceLog::HARD,
        shouldBlacklist: true,
        rawForDb: '{"blastengine":true}',
        rawPayload: [],
        server: $f['server'],
        useLegacyRecordHardBounce: true,
    );

    (new RecordBounce())->handle($event);

    // recordHardBounce writes a BounceLog row too — verify path produced one.
    expect(BounceLog::where('runtime_message_id', $f['msgId'])->exists())->toBeTrue();
});