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);
});