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