feat(notification): implement idempotent email delivery system and update related services
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<?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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user