Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions frontend/testing/unit/components/ApiKeySetupModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect, beforeEach, vi, type MockInstance } from 'vitest'
import ApiKeySetupModal from '../../../src/components/ApiKeySetupModal'

vi.mock('../../../src/api', () => ({
authenticateWithApiKey: vi.fn(),
}))

import { authenticateWithApiKey } from '../../../src/api'

describe('ApiKeySetupModal Error Handling', () => {
const defaultProps = {
onSaved: vi.fn(),
}

let consoleErrorSpy: MockInstance

beforeEach(() => {
vi.clearAllMocks()
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
})

it('renders the dialog with required elements', () => {
render(<ApiKeySetupModal {...defaultProps} />)

expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText('API Key Required')).toBeInTheDocument()
expect(screen.getByLabelText('Backend API Key')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /save and connect/i })).toBeInTheDocument()
})

it('handles successful API key validation', async () => {
const user = userEvent.setup()
vi.mocked(authenticateWithApiKey).mockResolvedValue(undefined)

render(<ApiKeySetupModal {...defaultProps} />)

const input = screen.getByLabelText('Backend API Key')
await user.type(input, 'valid-key-123')
await user.click(screen.getByRole('button', { name: /save and connect/i }))

expect(authenticateWithApiKey).toHaveBeenCalledWith('valid-key-123')
await waitFor(() => {
expect(defaultProps.onSaved).toHaveBeenCalled()
})
})

it('shows standard validation error on empty submission', async () => {
const user = userEvent.setup()
render(<ApiKeySetupModal {...defaultProps} />)

await user.click(screen.getByRole('button', { name: /save and connect/i }))

const alert = await screen.findByRole('alert')
expect(alert).toHaveTextContent('Please enter the API key.')
expect(authenticateWithApiKey).not.toHaveBeenCalled()
})

it('shows backend error message when api key is rejected', async () => {
const user = userEvent.setup()
const backendErrorMsg = 'Invalid/Expired API token'
vi.mocked(authenticateWithApiKey).mockRejectedValue(new Error(backendErrorMsg))

render(<ApiKeySetupModal {...defaultProps} />)

const input = screen.getByLabelText('Backend API Key')
await user.type(input, 'bad-key')
await user.click(screen.getByRole('button', { name: /save and connect/i }))

const alert = await screen.findByRole('alert')
expect(alert).toHaveTextContent(backendErrorMsg)
expect(defaultProps.onSaved).not.toHaveBeenCalled()
})

it('ensures sensitive key text is not logged or echoed in failure UI', async () => {
const user = userEvent.setup()
const sensitiveKey = 'SUPER_SECRET_TOKEN_999'
vi.mocked(authenticateWithApiKey).mockRejectedValue(new Error('Authentication failed. Check the API key.'))

render(<ApiKeySetupModal {...defaultProps} />)

const input = screen.getByLabelText('Backend API Key')
await user.type(input, sensitiveKey)
await user.click(screen.getByRole('button', { name: /save and connect/i }))

// Assert useful error message is visible
const alert = await screen.findByRole('alert')
expect(alert).toBeInTheDocument()

// Assert sensitive key text is NOT in the error message / failure UI text content
expect(alert.textContent).not.toContain(sensitiveKey)

// Assert sensitive key text is NOT logged in console.error
const errorCalls = consoleErrorSpy.mock.calls.flatMap((args) =>
args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
)
for (const logText of errorCalls) {
expect(logText).not.toContain(sensitiveKey)
}
})
})
Loading