LogDrainDestinationSheetForm.test.tsx233 lines · main
| 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 | |
| 6 | import { LogDrainDestinationSheetForm } from './LogDrainDestinationSheetForm' |
| 7 | import { render } from '@/tests/helpers' |
| 8 | |
| 9 | const { 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 | |
| 19 | vi.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 | |
| 30 | vi.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 | |
| 39 | vi.mock('@/lib/telemetry/track', () => ({ |
| 40 | useTrack: useTrackMock, |
| 41 | })) |
| 42 | |
| 43 | vi.mock('sonner', () => ({ |
| 44 | toast: { |
| 45 | error: vi.fn(), |
| 46 | success: vi.fn(), |
| 47 | }, |
| 48 | })) |
| 49 | |
| 50 | const 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 | |
| 69 | const submitForm = () => |
| 70 | fireEvent.submit(document.getElementById('log-drain-destination-form') as HTMLFormElement) |
| 71 | |
| 72 | describe('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 | }) |