- Add backend validation to detect and warn about anon vs service keys - Prevent startup with incorrect Supabase key configuration - Consolidate frontend state management following KISS principles - Remove duplicate state tracking and sessionStorage polling - Add clear error display when backend fails to start - Improve .env.example documentation with detailed key selection guide - Add comprehensive test coverage for validation logic - Remove unused test results checking to eliminate 404 errors The implementation now warns users about key misconfiguration while maintaining backward compatibility. Frontend state is simplified with MainLayout as the single source of truth for backend status.
236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
import { render, screen, fireEvent } from '@testing-library/react'
|
|
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import React from 'react'
|
|
import { credentialsService } from '../src/services/credentialsService'
|
|
|
|
describe('Error Handling Tests', () => {
|
|
test('api error simulation', () => {
|
|
const MockApiComponent = () => {
|
|
const [error, setError] = React.useState('')
|
|
const [loading, setLoading] = React.useState(false)
|
|
|
|
const fetchData = async () => {
|
|
setLoading(true)
|
|
try {
|
|
// Simulate API error
|
|
throw new Error('Network error')
|
|
} catch (err) {
|
|
setError('Failed to load data')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={fetchData}>Load Data</button>
|
|
{loading && <div>Loading...</div>}
|
|
{error && <div role="alert">{error}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<MockApiComponent />)
|
|
|
|
fireEvent.click(screen.getByText('Load Data'))
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Failed to load data')
|
|
})
|
|
|
|
test('timeout error simulation', () => {
|
|
const MockTimeoutComponent = () => {
|
|
const [status, setStatus] = React.useState('idle')
|
|
|
|
const handleTimeout = () => {
|
|
setStatus('loading')
|
|
setTimeout(() => {
|
|
setStatus('timeout')
|
|
}, 100)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={handleTimeout}>Start Request</button>
|
|
{status === 'loading' && <div>Loading...</div>}
|
|
{status === 'timeout' && <div role="alert">Request timed out</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<MockTimeoutComponent />)
|
|
|
|
fireEvent.click(screen.getByText('Start Request'))
|
|
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
|
|
// Wait for timeout
|
|
setTimeout(() => {
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Request timed out')
|
|
}, 150)
|
|
})
|
|
|
|
test('form validation errors', () => {
|
|
const MockFormErrors = () => {
|
|
const [values, setValues] = React.useState({ name: '', email: '' })
|
|
const [errors, setErrors] = React.useState<string[]>([])
|
|
|
|
const validate = () => {
|
|
const newErrors: string[] = []
|
|
if (!values.name) newErrors.push('Name is required')
|
|
if (!values.email) newErrors.push('Email is required')
|
|
if (values.email && !values.email.includes('@')) {
|
|
newErrors.push('Invalid email format')
|
|
}
|
|
setErrors(newErrors)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<input
|
|
placeholder="Name"
|
|
value={values.name}
|
|
onChange={(e) => setValues({ ...values, name: e.target.value })}
|
|
/>
|
|
<input
|
|
placeholder="Email"
|
|
value={values.email}
|
|
onChange={(e) => setValues({ ...values, email: e.target.value })}
|
|
/>
|
|
<button onClick={validate}>Submit</button>
|
|
{errors.length > 0 && (
|
|
<div role="alert">
|
|
{errors.map((error, index) => (
|
|
<div key={index}>{error}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<MockFormErrors />)
|
|
|
|
// Submit empty form
|
|
fireEvent.click(screen.getByText('Submit'))
|
|
|
|
const alert = screen.getByRole('alert')
|
|
expect(alert).toHaveTextContent('Name is required')
|
|
expect(alert).toHaveTextContent('Email is required')
|
|
})
|
|
|
|
test('connection error recovery', () => {
|
|
const MockConnection = () => {
|
|
const [connected, setConnected] = React.useState(true)
|
|
const [error, setError] = React.useState('')
|
|
|
|
const handleDisconnect = () => {
|
|
setConnected(false)
|
|
setError('Connection lost')
|
|
}
|
|
|
|
const handleReconnect = () => {
|
|
setConnected(true)
|
|
setError('')
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
|
|
{error && <div role="alert">{error}</div>}
|
|
<button onClick={handleDisconnect}>Simulate Disconnect</button>
|
|
<button onClick={handleReconnect}>Reconnect</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<MockConnection />)
|
|
|
|
expect(screen.getByText('Status: Connected')).toBeInTheDocument()
|
|
|
|
fireEvent.click(screen.getByText('Simulate Disconnect'))
|
|
expect(screen.getByText('Status: Disconnected')).toBeInTheDocument()
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Connection lost')
|
|
|
|
fireEvent.click(screen.getByText('Reconnect'))
|
|
expect(screen.getByText('Status: Connected')).toBeInTheDocument()
|
|
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
|
|
})
|
|
|
|
test('user friendly error messages', () => {
|
|
const MockErrorMessages = () => {
|
|
const [errorType, setErrorType] = React.useState('')
|
|
|
|
const getErrorMessage = (type: string) => {
|
|
switch (type) {
|
|
case '401':
|
|
return 'Please log in to continue'
|
|
case '403':
|
|
return "You don't have permission to access this"
|
|
case '404':
|
|
return "We couldn't find what you're looking for"
|
|
case '500':
|
|
return 'Something went wrong on our end'
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={() => setErrorType('401')}>401 Error</button>
|
|
<button onClick={() => setErrorType('403')}>403 Error</button>
|
|
<button onClick={() => setErrorType('404')}>404 Error</button>
|
|
<button onClick={() => setErrorType('500')}>500 Error</button>
|
|
{errorType && (
|
|
<div role="alert">{getErrorMessage(errorType)}</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<MockErrorMessages />)
|
|
|
|
fireEvent.click(screen.getByText('401 Error'))
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Please log in to continue')
|
|
|
|
fireEvent.click(screen.getByText('404 Error'))
|
|
expect(screen.getByRole('alert')).toHaveTextContent("We couldn't find what you're looking for")
|
|
|
|
fireEvent.click(screen.getByText('500 Error'))
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong on our end')
|
|
})
|
|
})
|
|
|
|
describe('CredentialsService Error Handling', () => {
|
|
const originalFetch = global.fetch
|
|
|
|
beforeEach(() => {
|
|
global.fetch = vi.fn() as any
|
|
})
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch
|
|
})
|
|
|
|
test('should handle network errors with context', async () => {
|
|
const mockError = new Error('Network request failed')
|
|
;(global.fetch as any).mockRejectedValueOnce(mockError)
|
|
|
|
await expect(credentialsService.createCredential({
|
|
key: 'TEST_KEY',
|
|
value: 'test',
|
|
is_encrypted: false,
|
|
category: 'test'
|
|
})).rejects.toThrow(/Network error while creating credential 'test_key'/)
|
|
})
|
|
|
|
test('should preserve context in error messages', async () => {
|
|
const mockError = new Error('database error')
|
|
;(global.fetch as any).mockRejectedValueOnce(mockError)
|
|
|
|
await expect(credentialsService.updateCredential({
|
|
key: 'OPENAI_API_KEY',
|
|
value: 'sk-test',
|
|
is_encrypted: true,
|
|
category: 'api_keys'
|
|
})).rejects.toThrow(/Updating credential 'OPENAI_API_KEY' failed/)
|
|
})
|
|
}) |