PlatformWebhooks.store.test.ts161 lines · main
1import { describe, expect, it } from 'vitest'
2
3import {
4 createInitialPlatformWebhooksState,
5 createWebhookEndpoint,
6 filterWebhookDeliveries,
7 filterWebhookEndpoints,
8 retryWebhookDelivery,
9 updateWebhookEndpoint,
10} from './PlatformWebhooks.store'
11import type { PlatformWebhooksState, WebhookEndpoint } from './PlatformWebhooks.types'
12
13const EMPTY_STATE: PlatformWebhooksState = {
14 endpoints: [],
15 deliveries: [],
16}
17
18const createEndpoint = (overrides?: Partial<WebhookEndpoint>): WebhookEndpoint => ({
19 id: '3c9b7e21-8d54-4f63-b2a1-6e7d8c9f0a12',
20 name: 'Billing events',
21 url: 'https://hooks.example.com/billing',
22 description: 'Invoices and receipts',
23 enabled: true,
24 eventTypes: ['project.updated'],
25 customHeaders: [],
26 createdBy: 'user@supabase.io',
27 createdAt: '2026-03-16T00:00:00.000Z',
28 ...overrides,
29})
30
31describe('PlatformWebhooks.store', () => {
32 it('stores an explicit non-empty endpoint name on create', () => {
33 const result = createWebhookEndpoint(
34 EMPTY_STATE,
35 {
36 name: ' Project analytics ',
37 url: 'https://hooks.example.com/analytics',
38 description: '',
39 enabled: true,
40 eventTypes: ['project.updated'],
41 customHeaders: [],
42 },
43 {
44 endpointId: '9e4b1d62-7c35-4f18-a9d1-2b6e7f8c9012',
45 signingSecret: 'whsec_example',
46 }
47 )
48
49 expect(result.endpoint.name).toBe('Project analytics')
50 })
51
52 it('allows an empty endpoint name on create', () => {
53 const result = createWebhookEndpoint(
54 EMPTY_STATE,
55 {
56 name: ' ',
57 url: 'https://hooks.example.com/fallback',
58 description: '',
59 enabled: true,
60 eventTypes: ['project.updated'],
61 customHeaders: [],
62 },
63 {
64 endpointId: '9e4b1d62-7c35-4f18-a9d1-2b6e7f8c9012',
65 signingSecret: 'whsec_example',
66 }
67 )
68
69 expect(result.endpoint.name).toBe('')
70 })
71
72 it('allows clearing an existing endpoint name on update', () => {
73 const state: PlatformWebhooksState = {
74 endpoints: [createEndpoint()],
75 deliveries: [],
76 }
77
78 const result = updateWebhookEndpoint(state, '3c9b7e21-8d54-4f63-b2a1-6e7d8c9f0a12', {
79 name: ' ',
80 url: 'https://hooks.example.com/billing',
81 description: 'Invoices and receipts',
82 enabled: true,
83 eventTypes: ['project.updated'],
84 customHeaders: [],
85 })
86
87 expect(result.endpoints[0].name).toBe('')
88 })
89
90 it('filters endpoints by name, url, and description', () => {
91 const endpoints = [
92 createEndpoint(),
93 createEndpoint({
94 id: '1a4e8c73-5b29-44af-8c62-9f1d2b3c4d5e',
95 name: '',
96 url: 'https://hooks.example.com/slack',
97 description: 'Operational alerts',
98 }),
99 ]
100
101 expect(filterWebhookEndpoints(endpoints, 'billing')).toEqual([endpoints[0]])
102 expect(filterWebhookEndpoints(endpoints, 'slack')).toEqual([endpoints[1]])
103 expect(filterWebhookEndpoints(endpoints, 'alerts')).toEqual([endpoints[1]])
104 })
105
106 it('retries an individual delivery by resetting it to pending with a new timestamp', () => {
107 const nextState = retryWebhookDelivery(
108 createInitialPlatformWebhooksState('project'),
109 'project-delivery-2',
110 { now: '2026-03-17T02:15:00.000Z' }
111 )
112
113 expect(nextState.deliveries.find((delivery) => delivery.id === 'project-delivery-2')).toEqual({
114 id: 'project-delivery-2',
115 endpointId: '3c9b7e21-8d54-4f63-b2a1-6e7d8c9f0a12',
116 eventType: 'project.resource_exhausted',
117 status: 'pending',
118 responseCode: undefined,
119 attemptAt: '2026-03-17T02:15:00.000Z',
120 })
121 })
122
123 it('returns endpoint deliveries sorted by latest attempt first after a retry', () => {
124 const nextState = retryWebhookDelivery(
125 createInitialPlatformWebhooksState('project'),
126 'project-delivery-2',
127 { now: '2026-03-17T02:15:00.000Z' }
128 )
129 const endpointDeliveries = filterWebhookDeliveries(
130 nextState.deliveries,
131 '3c9b7e21-8d54-4f63-b2a1-6e7d8c9f0a12',
132 ''
133 )
134
135 expect(endpointDeliveries).toHaveLength(8)
136 expect(endpointDeliveries.map((delivery) => delivery.id).slice(0, 3)).toEqual([
137 'project-delivery-2',
138 'project-delivery-1',
139 'project-delivery-3',
140 ])
141 expect(
142 endpointDeliveries.every((delivery, index) => {
143 if (index === 0) return true
144
145 return (
146 new Date(endpointDeliveries[index - 1].attemptAt).getTime() >=
147 new Date(delivery.attemptAt).getTime()
148 )
149 })
150 ).toBe(true)
151 })
152
153 it('does not retry successful deliveries', () => {
154 const initialState = createInitialPlatformWebhooksState('project')
155 const nextState = retryWebhookDelivery(initialState, 'project-delivery-1', {
156 now: '2026-03-17T02:15:00.000Z',
157 })
158
159 expect(nextState).toBe(initialState)
160 })
161})