whisper-money/tests/Feature/PostStripeEventToDiscordTes...

220 lines
7.1 KiB
PHP

<?php
use App\Listeners\PostStripeEventToDiscord;
use App\Services\Discord\DiscordWebhook;
use App\Services\Stripe\StripeCustomerResolver;
use Illuminate\Support\Facades\Http;
use Laravel\Cashier\Events\WebhookReceived;
beforeEach(function () {
config()->set('services.discord.webhook_url', 'https://discord.test/webhook');
});
function handleStripeWebhook(array $payload): void
{
$resolver = new class extends StripeCustomerResolver
{
public function label(?string $customerId): string
{
return $customerId === null ? 'unknown' : "Jane Doe ({$customerId})";
}
};
(new PostStripeEventToDiscord(app(DiscordWebhook::class), $resolver))
->handle(new WebhookReceived($payload));
}
/**
* @return array<int, array<string, mixed>>
*/
function sentFields(): array
{
$fields = [];
Http::assertSent(function ($request) use (&$fields) {
$fields = $request['embeds'][0]['fields'] ?? [];
return true;
});
return $fields;
}
test('posts a message for a created subscription with rich fields', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_1',
'type' => 'customer.subscription.created',
'data' => ['object' => [
'id' => 'sub_123',
'status' => 'active',
'customer' => 'cus_123',
'items' => ['data' => [['price' => [
'unit_amount' => 1999,
'currency' => 'eur',
'recurring' => ['interval' => 'month', 'interval_count' => 1],
]]]],
]],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'New subscription'));
$fields = collect(sentFields());
expect($fields->firstWhere('name', 'Customer')['value'])->toBe('Jane Doe (cus_123)');
expect($fields->firstWhere('name', 'Plan')['value'])->toBe('€19.99 / month');
expect($fields->firstWhere('name', 'Subscription')['value'])->toBe('sub_123');
});
test('posts the formatted amount for a succeeded payment', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_2',
'type' => 'invoice.payment_succeeded',
'data' => ['object' => ['amount_paid' => 1999, 'currency' => 'eur', 'customer_email' => 'a@b.com']],
]);
Http::assertSent(fn ($request) => collect($request['embeds'][0]['fields'])
->contains(fn ($field) => $field['value'] === '€19.99'));
});
test('skips a zero-amount payment so only the subscription message is posted', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_zero',
'type' => 'invoice.payment_succeeded',
'data' => ['object' => ['amount_paid' => 0, 'currency' => 'eur', 'customer_email' => 'a@b.com', 'subscription' => 'sub_123']],
]);
Http::assertNothingSent();
});
test('uses amount_due for a failed payment', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_3',
'type' => 'invoice.payment_failed',
'data' => ['object' => ['amount_due' => 500, 'currency' => 'usd']],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'Payment failed')
&& collect($request['embeds'][0]['fields'])->contains(fn ($field) => $field['value'] === '$5.00'));
});
test('ignores events outside the handled list', function () {
Http::fake();
handleStripeWebhook(['id' => 'evt_4', 'type' => 'charge.refunded', 'data' => ['object' => []]]);
Http::assertNothingSent();
});
test('skips duplicate deliveries of the same event id', function () {
Http::fake();
$payload = [
'id' => 'evt_dup',
'type' => 'customer.subscription.created',
'data' => ['object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1']],
];
handleStripeWebhook($payload);
handleStripeWebhook($payload);
Http::assertSentCount(1);
});
test('labels a scheduled cancellation distinctly from a deletion', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_5',
'type' => 'customer.subscription.updated',
'data' => [
'object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1', 'cancel_at_period_end' => true],
'previous_attributes' => ['cancel_at_period_end' => false],
],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'Cancellation scheduled'));
});
test('reports the status change on a meaningful update', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_6',
'type' => 'customer.subscription.updated',
'data' => [
'object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1'],
'previous_attributes' => ['status' => 'trialing'],
],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'status changed')
&& collect($request['embeds'][0]['fields'])
->contains(fn ($field) => $field['name'] === 'Changed' && str_contains($field['value'], 'trialing → active')));
});
test('reports the old and new plan on a plan change', function () {
Http::fake();
$price = fn (int $amount) => [
'items' => ['data' => [['price' => [
'unit_amount' => $amount,
'currency' => 'eur',
'recurring' => ['interval' => 'month', 'interval_count' => 1],
]]]],
];
handleStripeWebhook([
'id' => 'evt_plan_change',
'type' => 'customer.subscription.updated',
'data' => [
'object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1'] + $price(399),
'previous_attributes' => $price(999),
],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'Plan changed')
&& collect($request['embeds'][0]['fields'])
->contains(fn ($field) => $field['name'] === 'Changed' && str_contains($field['value'], '€9.99 / month → €3.99 / month')));
});
test('ignores trivial subscription updates', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_7',
'type' => 'customer.subscription.updated',
'data' => [
'object' => ['id' => 'sub_1', 'status' => 'active', 'customer' => 'cus_1'],
'previous_attributes' => ['latest_invoice' => 'in_123'],
],
]);
Http::assertNothingSent();
});
test('posts a cancellation for a deleted subscription with the reason', function () {
Http::fake();
handleStripeWebhook([
'id' => 'evt_8',
'type' => 'customer.subscription.deleted',
'data' => ['object' => [
'id' => 'sub_1',
'status' => 'canceled',
'customer' => 'cus_1',
'cancellation_details' => ['reason' => 'cancellation_requested'],
]],
]);
Http::assertSent(fn ($request) => str_contains($request['embeds'][0]['title'], 'Subscription cancelled')
&& collect($request['embeds'][0]['fields'])
->contains(fn ($field) => $field['name'] === 'Cancellation reason' && $field['value'] === 'cancellation requested'));
});