Skip to content

Commit 1dc22a9

Browse files
sadpandajoeclaude
andauthored
chore: add tests to DatabaseConnectionForm/EncryptedField (#34442)
Co-authored-by: Claude <[email protected]>
1 parent ad592c7 commit 1dc22a9

File tree

1 file changed

+369
-0
lines changed

1 file changed

+369
-0
lines changed
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { render, fireEvent, screen } from 'spec/helpers/testing-library';
21+
import { DatabaseObject, ConfigurationMethod } from '../../types';
22+
import { EncryptedField, encryptedCredentialsMap } from './EncryptedField';
23+
24+
// Mock the useToasts hook
25+
jest.mock('src/components/MessageToasts/withToasts', () => ({
26+
useToasts: () => ({
27+
addDangerToast: jest.fn(),
28+
}),
29+
}));
30+
31+
describe('EncryptedField', () => {
32+
// Test utilities
33+
const createMockDb = (
34+
engine: string | null | undefined,
35+
parameters: Record<string, any> = {},
36+
): DatabaseObject => ({
37+
configuration_method: ConfigurationMethod.DynamicForm,
38+
database_name: 'test-db',
39+
driver: 'test-driver',
40+
id: 1,
41+
name: 'Test Database',
42+
is_managed_externally: false,
43+
engine: engine as string | undefined,
44+
parameters,
45+
});
46+
47+
const createMockChangeMethods = () => ({
48+
onEncryptedExtraInputChange: jest.fn(),
49+
onParametersChange: jest.fn(),
50+
onChange: jest.fn(),
51+
onQueryChange: jest.fn(),
52+
onParametersUploadFileChange: jest.fn(),
53+
onAddTableCatalog: jest.fn(),
54+
onRemoveTableCatalog: jest.fn(),
55+
onExtraInputChange: jest.fn(),
56+
onSSHTunnelParametersChange: jest.fn(),
57+
});
58+
59+
// Helper function to assert onParametersChange calls
60+
const expectParametersChange = (
61+
changeMethods: ReturnType<typeof createMockChangeMethods>,
62+
fieldName: string | null | undefined,
63+
value: string,
64+
callIndex = 0,
65+
) => {
66+
expect(changeMethods.onParametersChange).toHaveBeenNthCalledWith(
67+
callIndex + 1,
68+
expect.objectContaining({
69+
target: expect.objectContaining({
70+
name: fieldName,
71+
value,
72+
}),
73+
}),
74+
);
75+
};
76+
77+
const defaultProps = {
78+
required: false,
79+
onParametersChange: jest.fn(),
80+
onParametersUploadFileChange: jest.fn(),
81+
changeMethods: createMockChangeMethods(),
82+
validationErrors: null,
83+
getValidation: jest.fn(),
84+
clearValidationErrors: jest.fn(),
85+
field: 'test',
86+
isValidating: false,
87+
isEditMode: false,
88+
editNewDb: false,
89+
db: createMockDb('gsheets'),
90+
};
91+
92+
// Use actual encryptedCredentialsMap for data-driven tests
93+
const supportedEngines = Object.entries(encryptedCredentialsMap);
94+
95+
beforeEach(() => {
96+
jest.clearAllMocks();
97+
});
98+
99+
describe('Engine-to-Field Mapping', () => {
100+
it.each(supportedEngines)(
101+
'resolves field name for %s engine → %s field',
102+
(engine, expectedField) => {
103+
const mockDb = createMockDb(engine);
104+
const props = { ...defaultProps, db: mockDb };
105+
106+
render(<EncryptedField {...props} />);
107+
108+
expectParametersChange(props.changeMethods, expectedField, '');
109+
expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
110+
},
111+
);
112+
113+
it('handles unmapped engines gracefully', () => {
114+
const unmappedEngine = 'unknown-engine-xyz';
115+
const mockDb = createMockDb(unmappedEngine);
116+
const props = { ...defaultProps, db: mockDb };
117+
118+
expect(() => render(<EncryptedField {...props} />)).not.toThrow();
119+
120+
expectParametersChange(props.changeMethods, undefined, '');
121+
expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
122+
});
123+
124+
it.each([
125+
['null engine', null, null],
126+
['undefined engine', undefined, undefined],
127+
['empty string engine', '', ''],
128+
])('handles %s gracefully', (_description, engine, expectedName) => {
129+
const mockDb = createMockDb(engine);
130+
const props = { ...defaultProps, db: mockDb };
131+
132+
expect(() => render(<EncryptedField {...props} />)).not.toThrow();
133+
134+
expectParametersChange(props.changeMethods, expectedName, '');
135+
expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
136+
});
137+
});
138+
139+
describe('Parameter Value Processing', () => {
140+
const testCases = [
141+
{
142+
input: { key: 'value', nested: { data: 'test' } },
143+
expected: '{"key":"value","nested":{"data":"test"}}',
144+
description: 'objects to JSON strings',
145+
},
146+
{
147+
input: true,
148+
expected: 'true',
149+
description: 'booleans to strings',
150+
},
151+
{
152+
input: false,
153+
expected: 'false',
154+
description: 'false booleans to strings',
155+
},
156+
{
157+
input: 'test-string',
158+
expected: 'test-string',
159+
description: 'string values unchanged',
160+
},
161+
{
162+
input: 123,
163+
expected: '123',
164+
description: 'numbers to strings',
165+
},
166+
];
167+
168+
it.each(testCases)(
169+
'processes $description correctly',
170+
({ input, expected }) => {
171+
const mockDb = createMockDb('gsheets', {
172+
service_account_info: input,
173+
});
174+
const props = { ...defaultProps, db: mockDb, isEditMode: true };
175+
176+
const { container } = render(<EncryptedField {...props} />);
177+
const textarea = container.querySelector('textarea');
178+
179+
expect(textarea?.value).toBe(expected);
180+
},
181+
);
182+
183+
it('handles null/undefined parameters', () => {
184+
const mockDb = createMockDb('gsheets', {});
185+
const props = { ...defaultProps, db: mockDb, isEditMode: true };
186+
187+
const { container } = render(<EncryptedField {...props} />);
188+
const textarea = container.querySelector('textarea');
189+
190+
expect(textarea?.value).toBe('');
191+
});
192+
});
193+
194+
describe('Conditional Rendering Logic', () => {
195+
it('shows upload selector in create mode', () => {
196+
const props = { ...defaultProps, isEditMode: false, editNewDb: false };
197+
198+
render(<EncryptedField {...props} />);
199+
200+
expect(
201+
screen.getByText(
202+
'How do you want to enter service account credentials?',
203+
),
204+
).toBeInTheDocument();
205+
expect(screen.getByRole('combobox')).toBeInTheDocument();
206+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
207+
});
208+
209+
it('shows textarea in edit mode', () => {
210+
const props = { ...defaultProps, isEditMode: true, editNewDb: false };
211+
212+
render(<EncryptedField {...props} />);
213+
214+
expect(
215+
screen.queryByText(
216+
'How do you want to enter service account credentials?',
217+
),
218+
).not.toBeInTheDocument();
219+
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
220+
expect(screen.getByRole('textbox')).toBeInTheDocument();
221+
});
222+
223+
it('shows textarea when editNewDb is true', () => {
224+
const props = { ...defaultProps, isEditMode: false, editNewDb: true };
225+
226+
render(<EncryptedField {...props} />);
227+
228+
// When editNewDb is true and isEditMode is false, both select and textarea are shown
229+
expect(
230+
screen.getByText(
231+
'How do you want to enter service account credentials?',
232+
),
233+
).toBeInTheDocument();
234+
expect(screen.getByRole('combobox')).toBeInTheDocument();
235+
expect(screen.getByRole('textbox')).toBeInTheDocument();
236+
});
237+
});
238+
239+
describe('Upload Option State Management', () => {
240+
it('defaults to upload option', () => {
241+
const props = { ...defaultProps, isEditMode: false };
242+
243+
render(<EncryptedField {...props} />);
244+
245+
expect(screen.getByText('Upload credentials')).toBeInTheDocument();
246+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
247+
});
248+
249+
it('switches to copy-paste option', () => {
250+
const props = { ...defaultProps, isEditMode: false };
251+
252+
render(<EncryptedField {...props} />);
253+
254+
const select = screen.getByRole('combobox');
255+
fireEvent.mouseDown(select);
256+
257+
const copyPasteOption = screen.getByText(
258+
'Copy and Paste JSON credentials',
259+
);
260+
fireEvent.click(copyPasteOption);
261+
262+
expect(screen.getByRole('textbox')).toBeInTheDocument();
263+
expect(screen.queryByText('Upload credentials')).not.toBeInTheDocument();
264+
});
265+
});
266+
267+
describe('Form Integration Contract', () => {
268+
it.each(supportedEngines)(
269+
'calls onParametersChange with correct field name for %s engine',
270+
(engine, fieldName) => {
271+
const mockDb = createMockDb(engine);
272+
const props = { ...defaultProps, db: mockDb, isEditMode: true };
273+
274+
render(<EncryptedField {...props} />);
275+
276+
const textarea = screen.getByRole('textbox');
277+
const testValue = 'test credential content';
278+
279+
fireEvent.change(textarea, { target: { value: testValue } });
280+
281+
expectParametersChange(props.changeMethods, fieldName, testValue, 1);
282+
},
283+
);
284+
285+
it('initializes with empty value on mount', () => {
286+
const props = { ...defaultProps };
287+
288+
render(<EncryptedField {...props} />);
289+
290+
expectParametersChange(
291+
props.changeMethods,
292+
'service_account_info', // gsheets default
293+
'',
294+
);
295+
});
296+
297+
it('renders correctly with default props', () => {
298+
const props = { ...defaultProps };
299+
300+
expect(() => render(<EncryptedField {...props} />)).not.toThrow();
301+
302+
// Should render upload UI by default
303+
expect(
304+
screen.getByText(
305+
'How do you want to enter service account credentials?',
306+
),
307+
).toBeInTheDocument();
308+
expect(screen.getByRole('combobox')).toBeInTheDocument();
309+
expect(screen.getByText('Upload credentials')).toBeInTheDocument();
310+
});
311+
});
312+
313+
describe('Error Boundaries', () => {
314+
it('renders gracefully when database prop is missing', () => {
315+
const props = { ...defaultProps, db: undefined };
316+
317+
expect(() => render(<EncryptedField {...props} />)).not.toThrow();
318+
319+
// Should still render the upload UI with undefined field name
320+
expectParametersChange(props.changeMethods, undefined, '');
321+
});
322+
323+
it('renders gracefully with malformed database parameters', () => {
324+
const mockDb = createMockDb('gsheets', {
325+
service_account_info: Symbol('test-symbol'),
326+
});
327+
const props = { ...defaultProps, db: mockDb, isEditMode: true };
328+
329+
expect(() => render(<EncryptedField {...props} />)).not.toThrow();
330+
331+
// Should still render textarea in edit mode
332+
const { container } = render(<EncryptedField {...props} />);
333+
const textarea = container.querySelector('textarea');
334+
expect(textarea).toBeInTheDocument();
335+
});
336+
});
337+
338+
describe('Accessibility', () => {
339+
it('provides proper form labels and attributes', () => {
340+
const props = { ...defaultProps, isEditMode: true };
341+
342+
render(<EncryptedField {...props} />);
343+
344+
expect(screen.getByText('Service Account')).toBeInTheDocument();
345+
346+
const textarea = screen.getByRole('textbox');
347+
expect(textarea).toHaveAttribute('name', 'service_account_info');
348+
expect(textarea).toHaveAttribute(
349+
'placeholder',
350+
'Paste content of service credentials JSON file here',
351+
);
352+
});
353+
354+
it('provides proper labels for upload method selection', () => {
355+
const props = { ...defaultProps, isEditMode: false };
356+
357+
render(<EncryptedField {...props} />);
358+
359+
expect(
360+
screen.getByText(
361+
'How do you want to enter service account credentials?',
362+
),
363+
).toBeInTheDocument();
364+
365+
const select = screen.getByRole('combobox');
366+
expect(select).toBeInTheDocument();
367+
});
368+
});
369+
});

0 commit comments

Comments
 (0)