feat: add ManualDNSChallenge component and related hooks for manual DNS challenge management

- Implemented `useManualChallenge`, `useChallengePoll`, and `useManualChallengeMutations` hooks for managing manual DNS challenges.
- Created tests for the `useManualChallenge` hooks to ensure correct fetching and mutation behavior.
- Added `ManualDNSChallenge` component for displaying challenge details and actions.
- Developed end-to-end tests for the Manual DNS Provider feature, covering provider selection, challenge UI, and accessibility compliance.
- Included error handling tests for verification failures and network errors.
This commit is contained in:
GitHub Actions
2026-01-12 04:01:40 +00:00
parent a199dfd079
commit d7939bed70
132 changed files with 8680 additions and 878 deletions

View File

@@ -0,0 +1,230 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
getChallenge,
createChallenge,
verifyChallenge,
pollChallenge,
deleteChallenge,
} from '../manualChallenge'
import client from '../client'
vi.mock('../client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}))
describe('manualChallenge API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getChallenge', () => {
it('fetches challenge by provider and challenge ID', async () => {
const mockChallenge = {
id: 'challenge-uuid',
status: 'pending',
fqdn: '_acme-challenge.example.com',
value: 'test-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockChallenge })
const result = await getChallenge(1, 'challenge-uuid')
expect(client.get).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid'
)
expect(result).toEqual(mockChallenge)
})
it('throws error when challenge not found', async () => {
vi.mocked(client.get).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Challenge not found' } },
})
await expect(getChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
response: { status: 404 },
})
})
})
describe('createChallenge', () => {
it('creates a new challenge for the provider', async () => {
const mockChallenge = {
id: 'new-challenge-uuid',
status: 'created',
fqdn: '_acme-challenge.example.com',
value: 'generated-value',
ttl: 300,
created_at: '2026-01-11T00:00:00Z',
expires_at: '2026-01-11T00:10:00Z',
dns_propagated: false,
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockChallenge })
const result = await createChallenge(1, { domain: 'example.com' })
expect(client.post).toHaveBeenCalledWith('/dns-providers/1/manual-challenge', {
domain: 'example.com',
})
expect(result).toEqual(mockChallenge)
})
it('throws error when provider not found', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Provider not found' } },
})
await expect(createChallenge(999, { domain: 'example.com' })).rejects.toMatchObject({
response: { status: 404 },
})
})
it('throws error when challenge already in progress', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 409, data: { code: 'CHALLENGE_IN_PROGRESS' } },
})
await expect(createChallenge(1, { domain: 'example.com' })).rejects.toMatchObject({
response: { status: 409 },
})
})
})
describe('verifyChallenge', () => {
it('triggers verification for a challenge', async () => {
const mockResult = {
success: true,
dns_found: true,
message: 'TXT record verified successfully',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
const result = await verifyChallenge(1, 'challenge-uuid')
expect(client.post).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid/verify'
)
expect(result).toEqual(mockResult)
})
it('returns dns_found false when record not propagated', async () => {
const mockResult = {
success: false,
dns_found: false,
message: 'DNS record not found',
}
vi.mocked(client.post).mockResolvedValueOnce({ data: mockResult })
const result = await verifyChallenge(1, 'challenge-uuid')
expect(result.success).toBe(false)
expect(result.dns_found).toBe(false)
})
it('throws error when challenge expired', async () => {
vi.mocked(client.post).mockRejectedValueOnce({
response: { status: 410, data: { code: 'CHALLENGE_EXPIRED' } },
})
await expect(verifyChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
response: { status: 410 },
})
})
})
describe('pollChallenge', () => {
it('returns current challenge status', async () => {
const mockPoll = {
status: 'pending',
dns_propagated: false,
time_remaining_seconds: 480,
last_check_at: '2026-01-11T00:02:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(client.get).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid/poll'
)
expect(result).toEqual(mockPoll)
})
it('returns verified status when DNS propagated', async () => {
const mockPoll = {
status: 'verified',
dns_propagated: true,
time_remaining_seconds: 0,
last_check_at: '2026-01-11T00:05:00Z',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(result.status).toBe('verified')
expect(result.dns_propagated).toBe(true)
})
it('includes error message when challenge failed', async () => {
const mockPoll = {
status: 'failed',
dns_propagated: false,
time_remaining_seconds: 0,
last_check_at: '2026-01-11T00:05:00Z',
error_message: 'ACME validation failed',
}
vi.mocked(client.get).mockResolvedValueOnce({ data: mockPoll })
const result = await pollChallenge(1, 'challenge-uuid')
expect(result.status).toBe('failed')
expect(result.error_message).toBe('ACME validation failed')
})
})
describe('deleteChallenge', () => {
it('deletes/cancels a challenge', async () => {
vi.mocked(client.delete).mockResolvedValueOnce({ data: undefined })
await deleteChallenge(1, 'challenge-uuid')
expect(client.delete).toHaveBeenCalledWith(
'/dns-providers/1/manual-challenge/challenge-uuid'
)
})
it('throws error when challenge not found', async () => {
vi.mocked(client.delete).mockRejectedValueOnce({
response: { status: 404, data: { error: 'Challenge not found' } },
})
await expect(deleteChallenge(1, 'invalid-uuid')).rejects.toMatchObject({
response: { status: 404 },
})
})
it('throws error when unauthorized', async () => {
vi.mocked(client.delete).mockRejectedValueOnce({
response: { status: 403, data: { error: 'Unauthorized' } },
})
await expect(deleteChallenge(1, 'challenge-uuid')).rejects.toMatchObject({
response: { status: 403 },
})
})
})
})