PlatformWebhooks.store.test.ts 4.7 KB

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