Files
shopit-back/tests/Feature/Notification/IdempotentEmailDeliveryServiceTest.php

137 lines
4.3 KiB
PHP

<?php
namespace Tests\Feature\Notification;
use App\Domains\Notification\Models\EmailDelivery;
use App\Domains\Notification\Services\IdempotentEmailDeliveryService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;
class IdempotentEmailDeliveryServiceTest extends TestCase
{
use RefreshDatabase;
public function test_it_sends_once_for_the_same_business_key(): void
{
$calls = 0;
$service = app(IdempotentEmailDeliveryService::class);
$first = $service->sendOnce(
'welcome:tenant:10',
'welcome',
'tenant',
['user_id' => 10],
'ada@example.com',
function () use (&$calls): void {
$calls++;
},
);
$second = $service->sendOnce(
'welcome:tenant:10',
'welcome',
'tenant',
['user_id' => 10],
'ada@example.com',
function () use (&$calls): void {
$calls++;
},
);
$this->assertTrue($first);
$this->assertFalse($second);
$this->assertSame(1, $calls);
$this->assertDatabaseHas('email_deliveries', [
'idempotency_key' => 'welcome:tenant:10',
'status' => EmailDelivery::STATUS_SENT,
'attempts' => 1,
]);
}
public function test_it_records_a_failure_and_allows_a_retry(): void
{
$service = app(IdempotentEmailDeliveryService::class);
try {
$service->sendOnce(
'purchase-confirmed:20',
'purchase_confirmed',
'tenant',
['purchase_id' => 20],
'buyer@example.com',
fn () => throw new RuntimeException('Sensitive SMTP detail'),
);
$this->fail('The delivery exception was not rethrown.');
} catch (RuntimeException) {
$this->assertDatabaseHas('email_deliveries', [
'idempotency_key' => 'purchase-confirmed:20',
'status' => EmailDelivery::STATUS_FAILED,
'attempts' => 1,
'last_error' => RuntimeException::class,
]);
}
$sent = $service->sendOnce(
'purchase-confirmed:20',
'purchase_confirmed',
'tenant',
['purchase_id' => 20],
'buyer@example.com',
static function (): void {},
);
$this->assertTrue($sent);
$this->assertDatabaseHas('email_deliveries', [
'idempotency_key' => 'purchase-confirmed:20',
'status' => EmailDelivery::STATUS_SENT,
'attempts' => 2,
'last_error' => null,
]);
}
public function test_it_recovers_an_expired_claim_but_not_an_active_one(): void
{
config(['mail.delivery_lease_seconds' => 300]);
$service = app(IdempotentEmailDeliveryService::class);
$delivery = EmailDelivery::query()->create([
'idempotency_key' => 'password-reset:30',
'email_type' => 'password_reset',
'tenant_code' => 'tenant',
'status' => EmailDelivery::STATUS_PROCESSING,
'attempts' => 1,
'context' => ['attempt_id' => 30],
'recipient_fingerprint' => str_repeat('a', 64),
'claim_token' => fake()->uuid(),
'claimed_at' => now(),
'lease_expires_at' => now()->addMinute(),
]);
$activeClaim = $service->sendOnce(
$delivery->idempotency_key,
$delivery->email_type,
$delivery->tenant_code,
$delivery->context,
'ada@example.com',
static function (): void {},
);
$this->assertFalse($activeClaim);
$delivery->update(['lease_expires_at' => now()->subSecond()]);
$expiredClaim = $service->sendOnce(
$delivery->idempotency_key,
$delivery->email_type,
$delivery->tenant_code,
$delivery->context,
'ada@example.com',
static function (): void {},
);
$this->assertTrue($expiredClaim);
$this->assertDatabaseHas('email_deliveries', [
'idempotency_key' => 'password-reset:30',
'status' => EmailDelivery::STATUS_SENT,
'attempts' => 2,
]);
}
}