137 lines
4.3 KiB
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,
|
|
]);
|
|
}
|
|
}
|