Files
Charon/frontend/src/api/__tests__/dnsProviders.test.ts
GitHub Actions 0cd93ceb79 fix(frontend): remove test types from base tsconfig for CI build
The base tsconfig.json had types: ["vitest/globals", "@testing-library/jest-dom/vitest"]
which are devDependencies only installed during development. CI production
builds with npm ci --production don't include these, causing TS2688 errors.

Solution:

Remove types array from tsconfig.json (let TS auto-discover available types)
Simplify tsconfig.build.json to only exclude test files
Add triple-slash type references to test setup file
Add typecheck config to vitest.config.ts
This ensures:

Production builds work without devDependencies
Test files still have proper type definitions
No JSX.IntrinsicElements errors from missing React types
2026-01-25 21:26:47 +00:00

432 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getDNSProviders,
getDNSProvider,
getDNSProviderTypes,
createDNSProvider,
updateDNSProvider,
deleteDNSProvider,
testDNSProvider,
testDNSProviderCredentials,
type DNSProvider,
type DNSProviderRequest,
type DNSProviderTypeInfo,
} from '../dnsProviders'
import client from '../client'
vi.mock('../client')
const mockProvider: DNSProvider = {
id: 1,
uuid: 'test-uuid-1',
name: 'Cloudflare Production',
provider_type: 'cloudflare',
enabled: true,
is_default: true,
has_credentials: true,
propagation_timeout: 120,
polling_interval: 2,
success_count: 5,
failure_count: 0,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
}
const mockProviderType: DNSProviderTypeInfo = {
type: 'cloudflare',
name: 'Cloudflare',
fields: [
{
name: 'api_token',
label: 'API Token',
type: 'password',
required: true,
hint: 'Cloudflare API token with DNS edit permissions',
},
],
documentation_url: 'https://developers.cloudflare.com/api/',
}
describe('getDNSProviders', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches all DNS providers successfully', async () => {
const mockProviders = [mockProvider, { ...mockProvider, id: 2, name: 'Secondary' }]
vi.mocked(client.get).mockResolvedValue({
data: { providers: mockProviders, total: 2 },
})
const result = await getDNSProviders()
expect(client.get).toHaveBeenCalledWith('/dns-providers')
expect(result).toEqual(mockProviders)
expect(result).toHaveLength(2)
})
it('returns empty array when no providers exist', async () => {
vi.mocked(client.get).mockResolvedValue({
data: { providers: [], total: 0 },
})
const result = await getDNSProviders()
expect(result).toEqual([])
})
it('handles network errors', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Network error'))
await expect(getDNSProviders()).rejects.toThrow('Network error')
})
it('handles server errors', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
await expect(getDNSProviders()).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('getDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches single provider by valid ID', async () => {
vi.mocked(client.get).mockResolvedValue({ data: mockProvider })
const result = await getDNSProvider(1)
expect(client.get).toHaveBeenCalledWith('/dns-providers/1')
expect(result).toEqual(mockProvider)
})
it('handles not found error for invalid ID', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 404 } })
await expect(getDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles server errors', async () => {
vi.mocked(client.get).mockRejectedValue({ response: { status: 500 } })
await expect(getDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('getDNSProviderTypes', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetches supported provider types with field definitions', async () => {
const mockTypes = [
mockProviderType,
{
type: 'route53',
name: 'AWS Route 53',
fields: [
{ name: 'access_key_id', label: 'Access Key ID', type: 'text', required: true },
{ name: 'secret_access_key', label: 'Secret Access Key', type: 'password', required: true },
],
documentation_url: 'https://aws.amazon.com/route53/',
} as DNSProviderTypeInfo,
]
vi.mocked(client.get).mockResolvedValue({
data: { types: mockTypes },
})
const result = await getDNSProviderTypes()
expect(client.get).toHaveBeenCalledWith('/dns-providers/types')
expect(result).toEqual(mockTypes)
expect(result).toHaveLength(2)
})
it('handles errors when fetching types', async () => {
vi.mocked(client.get).mockRejectedValue(new Error('Failed to fetch types'))
await expect(getDNSProviderTypes()).rejects.toThrow('Failed to fetch types')
})
})
describe('createDNSProvider', () => {
const validRequest: DNSProviderRequest = {
name: 'New Cloudflare',
provider_type: 'cloudflare',
credentials: { api_token: 'test-token-123' },
propagation_timeout: 120,
polling_interval: 2,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('creates provider successfully and returns with ID', async () => {
const createdProvider = { ...mockProvider, id: 5, name: 'New Cloudflare' }
vi.mocked(client.post).mockResolvedValue({ data: createdProvider })
const result = await createDNSProvider(validRequest)
expect(client.post).toHaveBeenCalledWith('/dns-providers', validRequest)
expect(result).toEqual(createdProvider)
expect(result.id).toBe(5)
})
it('handles validation error for missing required fields', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Missing required field: api_token' } },
})
await expect(
createDNSProvider({ ...validRequest, credentials: {} })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles validation error for invalid provider type', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Invalid provider type' } },
})
await expect(
createDNSProvider({ ...validRequest, provider_type: 'invalid' as DNSProviderRequest['provider_type'] })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles duplicate name error', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 409, data: { error: 'Provider with this name already exists' } },
})
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
response: { status: 409 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(createDNSProvider(validRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('updateDNSProvider', () => {
const updateRequest: DNSProviderRequest = {
name: 'Updated Name',
provider_type: 'cloudflare',
credentials: { api_token: 'new-token' },
}
beforeEach(() => {
vi.clearAllMocks()
})
it('updates provider successfully', async () => {
const updatedProvider = { ...mockProvider, name: 'Updated Name' }
vi.mocked(client.put).mockResolvedValue({ data: updatedProvider })
const result = await updateDNSProvider(1, updateRequest)
expect(client.put).toHaveBeenCalledWith('/dns-providers/1', updateRequest)
expect(result).toEqual(updatedProvider)
expect(result.name).toBe('Updated Name')
})
it('handles not found error', async () => {
vi.mocked(client.put).mockRejectedValue({ response: { status: 404 } })
await expect(updateDNSProvider(999, updateRequest)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles validation errors', async () => {
vi.mocked(client.put).mockRejectedValue({
response: { status: 400, data: { error: 'Invalid credentials' } },
})
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles server errors', async () => {
vi.mocked(client.put).mockRejectedValue({ response: { status: 500 } })
await expect(updateDNSProvider(1, updateRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('deleteDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('deletes provider successfully', async () => {
vi.mocked(client.delete).mockResolvedValue({ data: undefined })
await deleteDNSProvider(1)
expect(client.delete).toHaveBeenCalledWith('/dns-providers/1')
})
it('handles not found error', async () => {
vi.mocked(client.delete).mockRejectedValue({ response: { status: 404 } })
await expect(deleteDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles in-use error when provider used by proxy hosts', async () => {
vi.mocked(client.delete).mockRejectedValue({
response: {
status: 409,
data: { error: 'Cannot delete provider in use by proxy hosts' },
},
})
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
response: { status: 409 },
})
})
it('handles server errors', async () => {
vi.mocked(client.delete).mockRejectedValue({ response: { status: 500 } })
await expect(deleteDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('testDNSProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('returns success result with propagation time', async () => {
const successResult = {
success: true,
message: 'DNS challenge completed successfully',
propagation_time_ms: 1500,
}
vi.mocked(client.post).mockResolvedValue({ data: successResult })
const result = await testDNSProvider(1)
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/test')
expect(result).toEqual(successResult)
expect(result.success).toBe(true)
expect(result.propagation_time_ms).toBe(1500)
})
it('returns failure result with error message', async () => {
const failureResult = {
success: false,
error: 'Invalid API token',
code: 'AUTH_FAILED',
}
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
const result = await testDNSProvider(1)
expect(result).toEqual(failureResult)
expect(result.success).toBe(false)
expect(result.error).toBe('Invalid API token')
})
it('handles not found error', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 404 } })
await expect(testDNSProvider(999)).rejects.toMatchObject({
response: { status: 404 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(testDNSProvider(1)).rejects.toMatchObject({
response: { status: 500 },
})
})
})
describe('testDNSProviderCredentials', () => {
const testRequest: DNSProviderRequest = {
name: 'Test Provider',
provider_type: 'cloudflare',
credentials: { api_token: 'test-token' },
}
beforeEach(() => {
vi.clearAllMocks()
})
it('returns success for valid credentials', async () => {
const successResult = {
success: true,
message: 'Credentials validated successfully',
propagation_time_ms: 800,
}
vi.mocked(client.post).mockResolvedValue({ data: successResult })
const result = await testDNSProviderCredentials(testRequest)
expect(client.post).toHaveBeenCalledWith('/dns-providers/test', testRequest)
expect(result).toEqual(successResult)
expect(result.success).toBe(true)
})
it('returns failure for invalid credentials', async () => {
const failureResult = {
success: false,
error: 'Authentication failed',
code: 'INVALID_CREDENTIALS',
}
vi.mocked(client.post).mockResolvedValue({ data: failureResult })
const result = await testDNSProviderCredentials(testRequest)
expect(result).toEqual(failureResult)
expect(result.success).toBe(false)
})
it('handles validation errors for missing credentials', async () => {
vi.mocked(client.post).mockRejectedValue({
response: { status: 400, data: { error: 'Missing required field: api_token' } },
})
await expect(
testDNSProviderCredentials({ ...testRequest, credentials: {} })
).rejects.toMatchObject({
response: { status: 400 },
})
})
it('handles server errors', async () => {
vi.mocked(client.post).mockRejectedValue({ response: { status: 500 } })
await expect(testDNSProviderCredentials(testRequest)).rejects.toMatchObject({
response: { status: 500 },
})
})
})