You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2025/08/08 21:09:37 UTC

(superset) branch master updated: chore: add tests to DatabaseConnectionForm/EncryptedField (#34442)

This is an automated email from the ASF dual-hosted git repository.

jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 1dc22a9002 chore: add tests to DatabaseConnectionForm/EncryptedField (#34442)
1dc22a9002 is described below

commit 1dc22a90028fac336a1e58e142e2ad3a096e6934
Author: Joe Li <jo...@preset.io>
AuthorDate: Fri Aug 8 14:09:30 2025 -0700

    chore: add tests to DatabaseConnectionForm/EncryptedField (#34442)
    
    Co-authored-by: Claude <no...@anthropic.com>
---
 .../DatabaseConnectionForm/EncryptedField.test.tsx | 369 +++++++++++++++++++++
 1 file changed, 369 insertions(+)

diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.test.tsx
new file mode 100644
index 0000000000..c3fda7b735
--- /dev/null
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.test.tsx
@@ -0,0 +1,369 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { render, fireEvent, screen } from 'spec/helpers/testing-library';
+import { DatabaseObject, ConfigurationMethod } from '../../types';
+import { EncryptedField, encryptedCredentialsMap } from './EncryptedField';
+
+// Mock the useToasts hook
+jest.mock('src/components/MessageToasts/withToasts', () => ({
+  useToasts: () => ({
+    addDangerToast: jest.fn(),
+  }),
+}));
+
+describe('EncryptedField', () => {
+  // Test utilities
+  const createMockDb = (
+    engine: string | null | undefined,
+    parameters: Record<string, any> = {},
+  ): DatabaseObject => ({
+    configuration_method: ConfigurationMethod.DynamicForm,
+    database_name: 'test-db',
+    driver: 'test-driver',
+    id: 1,
+    name: 'Test Database',
+    is_managed_externally: false,
+    engine: engine as string | undefined,
+    parameters,
+  });
+
+  const createMockChangeMethods = () => ({
+    onEncryptedExtraInputChange: jest.fn(),
+    onParametersChange: jest.fn(),
+    onChange: jest.fn(),
+    onQueryChange: jest.fn(),
+    onParametersUploadFileChange: jest.fn(),
+    onAddTableCatalog: jest.fn(),
+    onRemoveTableCatalog: jest.fn(),
+    onExtraInputChange: jest.fn(),
+    onSSHTunnelParametersChange: jest.fn(),
+  });
+
+  // Helper function to assert onParametersChange calls
+  const expectParametersChange = (
+    changeMethods: ReturnType<typeof createMockChangeMethods>,
+    fieldName: string | null | undefined,
+    value: string,
+    callIndex = 0,
+  ) => {
+    expect(changeMethods.onParametersChange).toHaveBeenNthCalledWith(
+      callIndex + 1,
+      expect.objectContaining({
+        target: expect.objectContaining({
+          name: fieldName,
+          value,
+        }),
+      }),
+    );
+  };
+
+  const defaultProps = {
+    required: false,
+    onParametersChange: jest.fn(),
+    onParametersUploadFileChange: jest.fn(),
+    changeMethods: createMockChangeMethods(),
+    validationErrors: null,
+    getValidation: jest.fn(),
+    clearValidationErrors: jest.fn(),
+    field: 'test',
+    isValidating: false,
+    isEditMode: false,
+    editNewDb: false,
+    db: createMockDb('gsheets'),
+  };
+
+  // Use actual encryptedCredentialsMap for data-driven tests
+  const supportedEngines = Object.entries(encryptedCredentialsMap);
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('Engine-to-Field Mapping', () => {
+    it.each(supportedEngines)(
+      'resolves field name for %s engine → %s field',
+      (engine, expectedField) => {
+        const mockDb = createMockDb(engine);
+        const props = { ...defaultProps, db: mockDb };
+
+        render(<EncryptedField {...props} />);
+
+        expectParametersChange(props.changeMethods, expectedField, '');
+        expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
+      },
+    );
+
+    it('handles unmapped engines gracefully', () => {
+      const unmappedEngine = 'unknown-engine-xyz';
+      const mockDb = createMockDb(unmappedEngine);
+      const props = { ...defaultProps, db: mockDb };
+
+      expect(() => render(<EncryptedField {...props} />)).not.toThrow();
+
+      expectParametersChange(props.changeMethods, undefined, '');
+      expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
+    });
+
+    it.each([
+      ['null engine', null, null],
+      ['undefined engine', undefined, undefined],
+      ['empty string engine', '', ''],
+    ])('handles %s gracefully', (_description, engine, expectedName) => {
+      const mockDb = createMockDb(engine);
+      const props = { ...defaultProps, db: mockDb };
+
+      expect(() => render(<EncryptedField {...props} />)).not.toThrow();
+
+      expectParametersChange(props.changeMethods, expectedName, '');
+      expect(props.changeMethods.onParametersChange).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('Parameter Value Processing', () => {
+    const testCases = [
+      {
+        input: { key: 'value', nested: { data: 'test' } },
+        expected: '{"key":"value","nested":{"data":"test"}}',
+        description: 'objects to JSON strings',
+      },
+      {
+        input: true,
+        expected: 'true',
+        description: 'booleans to strings',
+      },
+      {
+        input: false,
+        expected: 'false',
+        description: 'false booleans to strings',
+      },
+      {
+        input: 'test-string',
+        expected: 'test-string',
+        description: 'string values unchanged',
+      },
+      {
+        input: 123,
+        expected: '123',
+        description: 'numbers to strings',
+      },
+    ];
+
+    it.each(testCases)(
+      'processes $description correctly',
+      ({ input, expected }) => {
+        const mockDb = createMockDb('gsheets', {
+          service_account_info: input,
+        });
+        const props = { ...defaultProps, db: mockDb, isEditMode: true };
+
+        const { container } = render(<EncryptedField {...props} />);
+        const textarea = container.querySelector('textarea');
+
+        expect(textarea?.value).toBe(expected);
+      },
+    );
+
+    it('handles null/undefined parameters', () => {
+      const mockDb = createMockDb('gsheets', {});
+      const props = { ...defaultProps, db: mockDb, isEditMode: true };
+
+      const { container } = render(<EncryptedField {...props} />);
+      const textarea = container.querySelector('textarea');
+
+      expect(textarea?.value).toBe('');
+    });
+  });
+
+  describe('Conditional Rendering Logic', () => {
+    it('shows upload selector in create mode', () => {
+      const props = { ...defaultProps, isEditMode: false, editNewDb: false };
+
+      render(<EncryptedField {...props} />);
+
+      expect(
+        screen.getByText(
+          'How do you want to enter service account credentials?',
+        ),
+      ).toBeInTheDocument();
+      expect(screen.getByRole('combobox')).toBeInTheDocument();
+      expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+    });
+
+    it('shows textarea in edit mode', () => {
+      const props = { ...defaultProps, isEditMode: true, editNewDb: false };
+
+      render(<EncryptedField {...props} />);
+
+      expect(
+        screen.queryByText(
+          'How do you want to enter service account credentials?',
+        ),
+      ).not.toBeInTheDocument();
+      expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
+      expect(screen.getByRole('textbox')).toBeInTheDocument();
+    });
+
+    it('shows textarea when editNewDb is true', () => {
+      const props = { ...defaultProps, isEditMode: false, editNewDb: true };
+
+      render(<EncryptedField {...props} />);
+
+      // When editNewDb is true and isEditMode is false, both select and textarea are shown
+      expect(
+        screen.getByText(
+          'How do you want to enter service account credentials?',
+        ),
+      ).toBeInTheDocument();
+      expect(screen.getByRole('combobox')).toBeInTheDocument();
+      expect(screen.getByRole('textbox')).toBeInTheDocument();
+    });
+  });
+
+  describe('Upload Option State Management', () => {
+    it('defaults to upload option', () => {
+      const props = { ...defaultProps, isEditMode: false };
+
+      render(<EncryptedField {...props} />);
+
+      expect(screen.getByText('Upload credentials')).toBeInTheDocument();
+      expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
+    });
+
+    it('switches to copy-paste option', () => {
+      const props = { ...defaultProps, isEditMode: false };
+
+      render(<EncryptedField {...props} />);
+
+      const select = screen.getByRole('combobox');
+      fireEvent.mouseDown(select);
+
+      const copyPasteOption = screen.getByText(
+        'Copy and Paste JSON credentials',
+      );
+      fireEvent.click(copyPasteOption);
+
+      expect(screen.getByRole('textbox')).toBeInTheDocument();
+      expect(screen.queryByText('Upload credentials')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('Form Integration Contract', () => {
+    it.each(supportedEngines)(
+      'calls onParametersChange with correct field name for %s engine',
+      (engine, fieldName) => {
+        const mockDb = createMockDb(engine);
+        const props = { ...defaultProps, db: mockDb, isEditMode: true };
+
+        render(<EncryptedField {...props} />);
+
+        const textarea = screen.getByRole('textbox');
+        const testValue = 'test credential content';
+
+        fireEvent.change(textarea, { target: { value: testValue } });
+
+        expectParametersChange(props.changeMethods, fieldName, testValue, 1);
+      },
+    );
+
+    it('initializes with empty value on mount', () => {
+      const props = { ...defaultProps };
+
+      render(<EncryptedField {...props} />);
+
+      expectParametersChange(
+        props.changeMethods,
+        'service_account_info', // gsheets default
+        '',
+      );
+    });
+
+    it('renders correctly with default props', () => {
+      const props = { ...defaultProps };
+
+      expect(() => render(<EncryptedField {...props} />)).not.toThrow();
+
+      // Should render upload UI by default
+      expect(
+        screen.getByText(
+          'How do you want to enter service account credentials?',
+        ),
+      ).toBeInTheDocument();
+      expect(screen.getByRole('combobox')).toBeInTheDocument();
+      expect(screen.getByText('Upload credentials')).toBeInTheDocument();
+    });
+  });
+
+  describe('Error Boundaries', () => {
+    it('renders gracefully when database prop is missing', () => {
+      const props = { ...defaultProps, db: undefined };
+
+      expect(() => render(<EncryptedField {...props} />)).not.toThrow();
+
+      // Should still render the upload UI with undefined field name
+      expectParametersChange(props.changeMethods, undefined, '');
+    });
+
+    it('renders gracefully with malformed database parameters', () => {
+      const mockDb = createMockDb('gsheets', {
+        service_account_info: Symbol('test-symbol'),
+      });
+      const props = { ...defaultProps, db: mockDb, isEditMode: true };
+
+      expect(() => render(<EncryptedField {...props} />)).not.toThrow();
+
+      // Should still render textarea in edit mode
+      const { container } = render(<EncryptedField {...props} />);
+      const textarea = container.querySelector('textarea');
+      expect(textarea).toBeInTheDocument();
+    });
+  });
+
+  describe('Accessibility', () => {
+    it('provides proper form labels and attributes', () => {
+      const props = { ...defaultProps, isEditMode: true };
+
+      render(<EncryptedField {...props} />);
+
+      expect(screen.getByText('Service Account')).toBeInTheDocument();
+
+      const textarea = screen.getByRole('textbox');
+      expect(textarea).toHaveAttribute('name', 'service_account_info');
+      expect(textarea).toHaveAttribute(
+        'placeholder',
+        'Paste content of service credentials JSON file here',
+      );
+    });
+
+    it('provides proper labels for upload method selection', () => {
+      const props = { ...defaultProps, isEditMode: false };
+
+      render(<EncryptedField {...props} />);
+
+      expect(
+        screen.getByText(
+          'How do you want to enter service account credentials?',
+        ),
+      ).toBeInTheDocument();
+
+      const select = screen.getByRole('combobox');
+      expect(select).toBeInTheDocument();
+    });
+  });
+});