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/ai.naniguide.com/tests/Unit/SendingServers/Webhooks/MailgunParseWebhookTest.php
<?php

use App\Model\BounceLog;
use App\Model\SendingServer;
use App\SendingServers\Drivers\Vendors\Mailgun\MailgunApiDriver;
use App\SendingServers\Webhooks\BounceReceived;
use App\SendingServers\Webhooks\BounceType;
use App\SendingServers\Webhooks\ComplaintReceived;
use App\SendingServers\Webhooks\FeedbackType;
use Illuminate\Http\Request;
use Tests\TestCase;

uses(TestCase::class);

/**
 * Parity test: MailgunApiDriver::parseWebhook() preserves byte-for-byte the DB
 * writes the legacy `SendingServerMailgun::handleNofification()` produced.
 *
 * Critical Mailgun-specific quirks (parity contract doc §4.1):
 *   - 'complained' BLACKLISTS subscriber (different from every other vendor — Q7)
 *   - 'complained' does NOT call markAsSpamReported (different from SendGrid)
 *   - BounceLog.raw / FeedbackLog.raw_feedback_content := full HTTP body
 *     (NOT just event-data — different from SendGrid which stored per-event JSON)
 *   - Missing signature/event-data/message → log warning + yield nothing
 */

function makeMailgunApiDriver(): MailgunApiDriver
{
    $server = new SendingServer(['type' => 'mailgun-api', 'name' => 'fixture']);
    $server->uid = 'fixture-uid';
    return new MailgunApiDriver($server);
}

function buildMailgunReq(string $fixturePath): Request
{
    $body = file_get_contents(__DIR__ . '/../../../Fixtures/Webhooks/mailgun/' . $fixturePath);
    return Request::create('/webhook/mailgun-api/fixture-uid', 'POST', [], [], [], [
        'CONTENT_TYPE' => 'application/json',
    ], $body);
}

test('mailgun failed → BounceReceived (HARD, blacklist, raw=full body)', function () {
    $driver = makeMailgunApiDriver();
    $rawBody = file_get_contents(__DIR__ . '/../../../Fixtures/Webhooks/mailgun/failed.json');

    $events = iterator_to_array($driver->parseWebhook(buildMailgunReq('failed.json')));

    expect($events)->toHaveCount(1)
        ->and($events[0])->toBeInstanceOf(BounceReceived::class)
        ->and($events[0]->type)->toBe(BounceType::HARD)
        ->and($events[0]->bounceTypeRaw)->toBe(BounceLog::HARD)
        ->and($events[0]->shouldBlacklist)->toBeTrue()
        ->and($events[0]->rawForDb)->toBe($rawBody);                  // PARITY: full body, not event-data
});

test('mailgun complained → ComplaintReceived (BLACKLIST=true, markSpamReported=false)', function () {
    $driver = makeMailgunApiDriver();
    $rawBody = file_get_contents(__DIR__ . '/../../../Fixtures/Webhooks/mailgun/complained.json');

    $events = iterator_to_array($driver->parseWebhook(buildMailgunReq('complained.json')));

    expect($events)->toHaveCount(1)
        ->and($events[0])->toBeInstanceOf(ComplaintReceived::class)
        ->and($events[0]->type)->toBe(FeedbackType::SPAM)
        ->and($events[0]->feedbackTypeRaw)->toBe('spam')
        // Q7: Mailgun-specific quirk — only vendor that blacklists on complaint.
        ->and($events[0]->shouldBlacklist)->toBeTrue()
        // PARITY: legacy did NOT call markAsSpamReported on Mailgun complained.
        ->and($events[0]->shouldMarkSpamReported)->toBeFalse()
        ->and($events[0]->rawForDb)->toBe($rawBody);
});

test('mailgun missing signature → yields nothing (parity)', function () {
    $driver = makeMailgunApiDriver();
    $events = iterator_to_array($driver->parseWebhook(buildMailgunReq('missing_signature.json')));

    // PARITY: legacy logged warning + did nothing.
    expect($events)->toBeEmpty();
});

test('mailgun message-id has angle brackets → cleaned up (StringHelper::cleanupMessageId)', function () {
    $driver = makeMailgunApiDriver();
    $events = iterator_to_array($driver->parseWebhook(buildMailgunReq('failed.json')));

    // Fixture uses `<[email protected]>`. cleanupMessageId
    // strips the < >. Verify the runtimeMessageId on the event doesn't have angle brackets.
    expect($events[0]->runtimeMessageId)->not->toContain('<')
        ->and($events[0]->runtimeMessageId)->not->toContain('>');
});

test('mailgun unknown event → IgnorableWebhookEvent (parity preserve)', function () {
    $driver = makeMailgunApiDriver();
    $body = json_encode([
        'signature' => ['timestamp' => '1', 'token' => 't', 'signature' => 's'],
        'event-data' => [
            'event' => 'opened',
            'message' => ['headers' => ['message-id' => '<[email protected]>']],
        ],
    ]);
    $req = Request::create('/webhook/mailgun-api/fixture-uid', 'POST', [], [], [], [
        'CONTENT_TYPE' => 'application/json',
    ], $body);

    $events = iterator_to_array($driver->parseWebhook($req));

    // PARITY: legacy code had no `else` branch — events other than 'failed'/'complained'
    // silently fell through (no DB write). Yield IgnorableWebhookEvent.
    expect($events)->toHaveCount(1)
        ->and($events[0])->toBeInstanceOf(\App\SendingServers\Webhooks\IgnorableWebhookEvent::class);
});