LogDrainDestinationSheetForm.test.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { fireEvent, screen, waitFor } from '@testing-library/react'
  2. import userEvent from '@testing-library/user-event'
  3. import type { ComponentProps } from 'react'
  4. import { beforeEach, describe, expect, it, vi } from 'vitest'
  5. import { LogDrainDestinationSheetForm } from './LogDrainDestinationSheetForm'
  6. import { render } from '@/tests/helpers'
  7. const { trackMock, useFlagMock, useLogDrainsQueryMock, useParamsMock, useTrackMock } = vi.hoisted(
  8. () => ({
  9. trackMock: vi.fn(),
  10. useFlagMock: vi.fn(),
  11. useLogDrainsQueryMock: vi.fn(),
  12. useParamsMock: vi.fn(),
  13. useTrackMock: vi.fn(),
  14. })
  15. )
  16. vi.mock(import('common'), async (importOriginal) => {
  17. const actual = await importOriginal()
  18. return {
  19. ...actual,
  20. IS_PLATFORM: false,
  21. useFlag: useFlagMock,
  22. useParams: useParamsMock,
  23. }
  24. })
  25. vi.mock(import('@/data/log-drains/log-drains-query'), async (importOriginal) => {
  26. const actual = await importOriginal()
  27. return {
  28. ...actual,
  29. useLogDrainsQuery: useLogDrainsQueryMock,
  30. }
  31. })
  32. vi.mock('@/lib/telemetry/track', () => ({
  33. useTrack: useTrackMock,
  34. }))
  35. vi.mock('sonner', () => ({
  36. toast: {
  37. error: vi.fn(),
  38. success: vi.fn(),
  39. },
  40. }))
  41. const renderForm = (props?: Partial<ComponentProps<typeof LogDrainDestinationSheetForm>>) => {
  42. const onOpenChange = vi.fn()
  43. const onSubmit = vi.fn()
  44. render(
  45. <LogDrainDestinationSheetForm
  46. open
  47. mode="create"
  48. isLoading={false}
  49. onOpenChange={onOpenChange}
  50. onSubmit={onSubmit}
  51. defaultValues={{ type: 'webhook' }}
  52. {...props}
  53. />
  54. )
  55. return { onOpenChange, onSubmit }
  56. }
  57. const submitForm = () =>
  58. fireEvent.submit(document.getElementById('log-drain-destination-form') as HTMLFormElement)
  59. describe('LogDrainDestinationSheetForm', () => {
  60. beforeEach(() => {
  61. vi.clearAllMocks()
  62. useFlagMock.mockReturnValue(true)
  63. useParamsMock.mockReturnValue({ ref: 'project-ref' })
  64. useLogDrainsQueryMock.mockReturnValue({ data: [] })
  65. useTrackMock.mockReturnValue(trackMock)
  66. })
  67. it('shows the JSON content type header for webhook create mode', async () => {
  68. renderForm()
  69. await screen.findByRole('dialog')
  70. expect(screen.getByDisplayValue('Content-Type')).toBeInTheDocument()
  71. expect(screen.getByDisplayValue('application/json')).toBeInTheDocument()
  72. })
  73. it('shows the protobuf content type header for OTLP create mode', async () => {
  74. renderForm({
  75. defaultValues: { type: 'otlp' },
  76. })
  77. await screen.findByRole('dialog')
  78. expect(screen.getByDisplayValue('Content-Type')).toBeInTheDocument()
  79. expect(screen.getByDisplayValue('application/x-protobuf')).toBeInTheDocument()
  80. })
  81. it('submits headers as a record without leaking the internal headerEntries field', async () => {
  82. const user = userEvent.setup()
  83. const { onSubmit } = renderForm()
  84. await screen.findByRole('dialog')
  85. await user.type(screen.getByPlaceholderText('My Destination'), 'Webhook sink')
  86. await user.type(
  87. screen.getByPlaceholderText('https://example.com/log-drain'),
  88. 'https://logs.example.com/ingest'
  89. )
  90. await user.click(screen.getByRole('button', { name: 'Add a new header' }))
  91. const headerNameInputs = screen.getAllByPlaceholderText('Header name')
  92. const headerValueInputs = screen.getAllByPlaceholderText('Header value')
  93. await user.type(headerNameInputs[1], 'X-API-Key')
  94. await user.type(headerValueInputs[1], 'secret-key')
  95. submitForm()
  96. await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
  97. expect(onSubmit).toHaveBeenCalledWith(
  98. expect.objectContaining({
  99. type: 'webhook',
  100. headers: {
  101. 'Content-Type': 'application/json',
  102. 'X-API-Key': 'secret-key',
  103. },
  104. })
  105. )
  106. expect(onSubmit.mock.calls[0][0]).not.toHaveProperty('headerEntries')
  107. })
  108. it('rejects duplicate header names with an inline field error', async () => {
  109. const user = userEvent.setup()
  110. const { onSubmit } = renderForm()
  111. await screen.findByRole('dialog')
  112. await user.type(screen.getByPlaceholderText('My Destination'), 'Webhook sink')
  113. await user.type(
  114. screen.getByPlaceholderText('https://example.com/log-drain'),
  115. 'https://logs.example.com/ingest'
  116. )
  117. await user.click(screen.getByRole('button', { name: 'Add a new header' }))
  118. const headerNameInputs = screen.getAllByPlaceholderText('Header name')
  119. const headerValueInputs = screen.getAllByPlaceholderText('Header value')
  120. await user.type(headerNameInputs[1], 'Content-Type')
  121. await user.type(headerValueInputs[1], 'application/custom')
  122. submitForm()
  123. expect((await screen.findAllByText('Header name already exists')).length).toBeGreaterThan(0)
  124. expect(onSubmit).not.toHaveBeenCalled()
  125. })
  126. it('allows fully empty header rows without blocking submit', async () => {
  127. const user = userEvent.setup()
  128. const { onSubmit } = renderForm()
  129. await screen.findByRole('dialog')
  130. await user.type(screen.getByPlaceholderText('My Destination'), 'Webhook sink')
  131. await user.type(
  132. screen.getByPlaceholderText('https://example.com/log-drain'),
  133. 'https://logs.example.com/ingest'
  134. )
  135. await user.click(screen.getByRole('button', { name: 'Add a new header' }))
  136. submitForm()
  137. await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1))
  138. expect(onSubmit).toHaveBeenCalledWith(
  139. expect.objectContaining({
  140. type: 'webhook',
  141. headers: {
  142. 'Content-Type': 'application/json',
  143. },
  144. })
  145. )
  146. expect(screen.queryByText('undefined')).not.toBeInTheDocument()
  147. })
  148. it('shows an inline value error when a header key is entered without a value', async () => {
  149. const user = userEvent.setup()
  150. const { onSubmit } = renderForm()
  151. await screen.findByRole('dialog')
  152. await user.type(screen.getByPlaceholderText('My Destination'), 'Webhook sink')
  153. await user.type(
  154. screen.getByPlaceholderText('https://example.com/log-drain'),
  155. 'https://logs.example.com/ingest'
  156. )
  157. await user.click(screen.getByRole('button', { name: 'Add a new header' }))
  158. const headerNameInputs = screen.getAllByPlaceholderText('Header name')
  159. await user.type(headerNameInputs[1], 'X-Draft-Only')
  160. submitForm()
  161. expect(await screen.findByText('Header value is required')).toBeInTheDocument()
  162. expect(onSubmit).not.toHaveBeenCalled()
  163. expect(screen.queryByText('undefined')).not.toBeInTheDocument()
  164. })
  165. it('shows an inline key error when a header value is entered without a key', async () => {
  166. const user = userEvent.setup()
  167. const { onSubmit } = renderForm()
  168. await screen.findByRole('dialog')
  169. await user.type(screen.getByPlaceholderText('My Destination'), 'Webhook sink')
  170. await user.type(
  171. screen.getByPlaceholderText('https://example.com/log-drain'),
  172. 'https://logs.example.com/ingest'
  173. )
  174. await user.click(screen.getByRole('button', { name: 'Add a new header' }))
  175. const headerValueInputs = screen.getAllByPlaceholderText('Header value')
  176. await user.type(headerValueInputs[1], 'draft-value')
  177. submitForm()
  178. expect(await screen.findByText('Header name is required')).toBeInTheDocument()
  179. expect(onSubmit).not.toHaveBeenCalled()
  180. expect(screen.queryByText('undefined')).not.toBeInTheDocument()
  181. })
  182. })