Files
insforge--insforge/backend/tests/unit/stripe-webhook.service.test.ts
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

135 lines
3.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { StripeEvent } from '../../src/types/payments';
const { mockPool } = vi.hoisted(() => ({
mockPool: {
query: vi.fn(),
},
}));
vi.mock('../../src/infra/database/database.manager', () => ({
DatabaseManager: {
getInstance: () => ({
getPool: () => mockPool,
}),
},
}));
import { StripeWebhookService } from '../../src/services/payments/stripe/webhook.service';
describe('StripeWebhookService', () => {
beforeEach(() => {
vi.clearAllMocks();
mockPool.query.mockReset();
});
it('does not reprocess duplicate webhook events that are still pending', async () => {
const freshPendingUpdatedAt = new Date();
mockPool.query
.mockResolvedValueOnce({ rows: [] })
.mockResolvedValueOnce({ rows: [] })
.mockResolvedValueOnce({
rows: [
{
environment: 'test',
eventId: 'evt_123',
eventType: 'checkout.session.completed',
livemode: false,
accountId: null,
objectType: 'checkout.session',
objectId: 'cs_test_123',
processingStatus: 'pending',
attemptCount: 1,
lastError: null,
receivedAt: new Date('2026-04-30T00:00:00.000Z'),
processedAt: null,
createdAt: new Date('2026-04-30T00:00:00.000Z'),
updatedAt: freshPendingUpdatedAt,
},
],
});
const result = await StripeWebhookService.getInstance().recordWebhookEventStart('test', {
id: 'evt_123',
type: 'checkout.session.completed',
livemode: false,
account: null,
data: {
object: {
id: 'cs_test_123',
object: 'checkout.session',
},
},
} as StripeEvent);
expect(result).toMatchObject({
shouldProcess: false,
row: {
eventId: 'evt_123',
processingStatus: 'pending',
},
});
expect(
mockPool.query.mock.calls.some(([sql]) =>
/UPDATE payments\.webhook_events/i.test(String(sql))
)
).toBe(true);
expect(mockPool.query).toHaveBeenNthCalledWith(
2,
expect.stringMatching(
/UPDATE payments\.webhook_events[\s\S]*processing_status = 'failed'[\s\S]*OR \(processing_status = 'pending' AND updated_at < \$5\)/i
),
['stripe', 'test', 'evt_123', expect.any(Object), expect.any(Date)]
);
});
it('reclaims stale pending webhook events for retry', async () => {
const reclaimedUpdatedAt = new Date();
mockPool.query.mockResolvedValueOnce({ rows: [] }).mockResolvedValueOnce({
rows: [
{
environment: 'test',
eventId: 'evt_123',
eventType: 'checkout.session.completed',
livemode: false,
accountId: null,
objectType: 'checkout.session',
objectId: 'cs_test_123',
processingStatus: 'pending',
attemptCount: 2,
lastError: null,
receivedAt: new Date('2026-04-30T00:00:00.000Z'),
processedAt: null,
createdAt: new Date('2026-04-30T00:00:00.000Z'),
updatedAt: reclaimedUpdatedAt,
},
],
});
const result = await StripeWebhookService.getInstance().recordWebhookEventStart('test', {
id: 'evt_123',
type: 'checkout.session.completed',
livemode: false,
account: null,
data: {
object: {
id: 'cs_test_123',
object: 'checkout.session',
},
},
} as StripeEvent);
expect(result).toMatchObject({
shouldProcess: true,
row: {
eventId: 'evt_123',
processingStatus: 'pending',
attemptCount: 2,
},
});
expect(mockPool.query).toHaveBeenCalledTimes(2);
});
});