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/SparkPostParseWebhookTest.php
<?php

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

uses(TestCase::class);

/**
 * Wave D.3 parity: SparkPost has special quirks
 *   - Ping handshake body `[{"msys":{}}]` (json_decode → [['msys' => []]]).
 *   - Three different event types ('bounce','out_of_band','policy_rejection')
 *     ALL map to BounceLog::HARD historically (legacy parity).
 *   - Missing rcpt_meta.runtime_message_id silently swallowed (Q3 quirk).
 */

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

function spReq(array $body): Request
{
    return Request::create('/webhook/sparkpost-api/fixture-uid', 'POST', [], [], [], [
        'CONTENT_TYPE' => 'application/json',
    ], json_encode($body));
}

function spEvent(string $type, ?string $msgId = 'fixture-msg'): array
{
    return [
        'msys' => [
            'message_event' => [
                'type' => $type,
                'rcpt_meta' => $msgId !== null ? ['runtime_message_id' => $msgId] : [],
            ],
        ],
    ];
}

test('sparkpost ping body [[msys:[]]] → no events (handshake)', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([['msys' => []]])));

    expect($events)->toBeEmpty();
});

test('sparkpost bounce → BounceReceived (HARD)', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('bounce')])));

    expect($events)->toHaveCount(1)
        ->and($events[0])->toBeInstanceOf(BounceReceived::class)
        ->and($events[0]->type)->toBe(BounceType::HARD)
        ->and($events[0]->bounceTypeRaw)->toBe(BounceLog::HARD);
});

test('sparkpost out_of_band → BounceReceived HARD (legacy parity)', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('out_of_band')])));

    expect($events[0])->toBeInstanceOf(BounceReceived::class)
        ->and($events[0]->type)->toBe(BounceType::HARD);
});

test('sparkpost policy_rejection → BounceReceived HARD (legacy parity)', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('policy_rejection')])));

    expect($events[0])->toBeInstanceOf(BounceReceived::class)
        ->and($events[0]->type)->toBe(BounceType::HARD);
});

test('sparkpost spam_complaint → ComplaintReceived', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('spam_complaint')])));

    expect($events[0])->toBeInstanceOf(ComplaintReceived::class)
        ->and($events[0]->type)->toBe(FeedbackType::SPAM)
        ->and($events[0]->shouldBlacklist)->toBeFalse()
        ->and($events[0]->shouldMarkSpamReported)->toBeTrue();
});

test('sparkpost missing rcpt_meta runtime_message_id → silent skip (Q3 quirk)', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('bounce', null)])));

    expect($events)->toBeEmpty();
});

test('sparkpost event without message_event → log warning + skip', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([['msys' => ['track_event' => ['type' => 'open']]]])));

    expect($events)->toBeEmpty();
});

test('sparkpost unknown event type → IgnorableWebhookEvent', function () {
    $driver = makeSparkPostDriver();
    $events = iterator_to_array($driver->parseWebhook(spReq([spEvent('made_up_event')])));

    expect($events[0])->toBeInstanceOf(IgnorableWebhookEvent::class);
});