diff --git a/frontend/src/api/__tests__/client.test.ts b/frontend/src/api/__tests__/client.test.ts index adf02f31..1b161ff9 100644 --- a/frontend/src/api/__tests__/client.test.ts +++ b/frontend/src/api/__tests__/client.test.ts @@ -130,9 +130,10 @@ describe('api client', () => { expect(handler).toBeDefined() // Call handler with auth endpoint error to verify it skips the auth error handler - if (handler) { - await handler(error) - } + const resultPromise = handler ? handler(error) : Promise.reject(new Error('handler missing')) + + await expect(resultPromise).rejects.toBe(error) + expect(onAuthError).not.toHaveBeenCalled() warnSpy.mockRestore() }) diff --git a/frontend/src/api/__tests__/import.test.ts b/frontend/src/api/__tests__/import.test.ts index 5e4fd048..11485771 100644 --- a/frontend/src/api/__tests__/import.test.ts +++ b/frontend/src/api/__tests__/import.test.ts @@ -10,6 +10,9 @@ vi.mock('../client', () => ({ })); describe('import API', () => { + const mockedGet = vi.mocked(client.get); + const mockedPost = vi.mocked(client.post); + beforeEach(() => { vi.clearAllMocks(); }); @@ -17,7 +20,7 @@ describe('import API', () => { it('uploadCaddyfile posts content', async () => { const content = 'example.com'; const mockResponse = { preview: { hosts: [] } }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await uploadCaddyfile(content); expect(client.post).toHaveBeenCalledWith('/import/upload', { content }); @@ -27,7 +30,7 @@ describe('import API', () => { it('uploadCaddyfilesMulti posts files', async () => { const files = [{ filename: 'Caddyfile', content: 'foo.com' }]; const mockResponse = { preview: { hosts: [] } }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await uploadCaddyfilesMulti(files); expect(client.post).toHaveBeenCalledWith('/import/upload-multi', { files }); @@ -36,7 +39,7 @@ describe('import API', () => { it('getImportPreview gets preview', async () => { const mockResponse = { preview: { hosts: [] } }; - (client.get as any).mockResolvedValue({ data: mockResponse }); + mockedGet.mockResolvedValue({ data: mockResponse }); const result = await getImportPreview(); expect(client.get).toHaveBeenCalledWith('/import/preview'); @@ -49,7 +52,7 @@ describe('import API', () => { const names = { 'foo.com': 'My Site' }; const mockResponse = { created: 1, updated: 0, skipped: 0, errors: [] }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await commitImport(sessionUUID, resolutions, names); expect(client.post).toHaveBeenCalledWith('/import/commit', { @@ -61,7 +64,7 @@ describe('import API', () => { }); it('cancelImport posts cancel', async () => { - (client.post as any).mockResolvedValue({}); + mockedPost.mockResolvedValue({}); await cancelImport(); expect(client.post).toHaveBeenCalledWith('/import/cancel'); @@ -69,7 +72,7 @@ describe('import API', () => { it('getImportStatus gets status', async () => { const mockResponse = { has_pending: true }; - (client.get as any).mockResolvedValue({ data: mockResponse }); + mockedGet.mockResolvedValue({ data: mockResponse }); const result = await getImportStatus(); expect(client.get).toHaveBeenCalledWith('/import/status'); @@ -77,7 +80,7 @@ describe('import API', () => { }); it('getImportStatus handles error', async () => { - (client.get as any).mockRejectedValue(new Error('Failed')); + mockedGet.mockRejectedValue(new Error('Failed')); const result = await getImportStatus(); expect(result).toEqual({ has_pending: false }); diff --git a/frontend/src/api/__tests__/logs-websocket.test.ts b/frontend/src/api/__tests__/logs-websocket.test.ts index 912db2df..1fb98906 100644 --- a/frontend/src/api/__tests__/logs-websocket.test.ts +++ b/frontend/src/api/__tests__/logs-websocket.test.ts @@ -54,8 +54,7 @@ describe('logs API - connectLiveLogs', () => { beforeEach(() => { // Mock global WebSocket mockWebSocket = new MockWebSocket(''); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (globalThis as any).WebSocket = class MockedWebSocket extends MockWebSocket { + (globalThis as typeof globalThis & { WebSocket: typeof WebSocket }).WebSocket = class MockedWebSocket extends MockWebSocket { constructor(url: string) { super(url); // eslint-disable-next-line @typescript-eslint/no-this-alias diff --git a/frontend/src/api/__tests__/securityHeaders.test.ts b/frontend/src/api/__tests__/securityHeaders.test.ts index 145aeb94..3fe6a5b8 100644 --- a/frontend/src/api/__tests__/securityHeaders.test.ts +++ b/frontend/src/api/__tests__/securityHeaders.test.ts @@ -12,13 +12,18 @@ vi.mock('../client', () => ({ })); describe('securityHeadersApi', () => { + const mockedGet = vi.mocked(client.get); + const mockedPost = vi.mocked(client.post); + const mockedPut = vi.mocked(client.put); + const mockedDelete = vi.mocked(client.delete); + beforeEach(() => { vi.clearAllMocks(); }); it('listProfiles returns profiles', async () => { const mockProfiles = [{ id: 1, name: 'Profile 1' }]; - (client.get as any).mockResolvedValue({ data: { profiles: mockProfiles } }); + mockedGet.mockResolvedValue({ data: { profiles: mockProfiles } }); const result = await securityHeadersApi.listProfiles(); expect(client.get).toHaveBeenCalledWith('/security/headers/profiles'); @@ -27,7 +32,7 @@ describe('securityHeadersApi', () => { it('getProfile returns a profile', async () => { const mockProfile = { id: 1, name: 'Profile 1' }; - (client.get as any).mockResolvedValue({ data: { profile: mockProfile } }); + mockedGet.mockResolvedValue({ data: { profile: mockProfile } }); const result = await securityHeadersApi.getProfile(1); expect(client.get).toHaveBeenCalledWith('/security/headers/profiles/1'); @@ -37,7 +42,7 @@ describe('securityHeadersApi', () => { it('createProfile creates a profile', async () => { const newProfile = { name: 'New Profile' }; const mockResponse = { id: 1, ...newProfile }; - (client.post as any).mockResolvedValue({ data: { profile: mockResponse } }); + mockedPost.mockResolvedValue({ data: { profile: mockResponse } }); const result = await securityHeadersApi.createProfile(newProfile); expect(client.post).toHaveBeenCalledWith('/security/headers/profiles', newProfile); @@ -47,7 +52,7 @@ describe('securityHeadersApi', () => { it('updateProfile updates a profile', async () => { const updates = { name: 'Updated Profile' }; const mockResponse = { id: 1, ...updates }; - (client.put as any).mockResolvedValue({ data: { profile: mockResponse } }); + mockedPut.mockResolvedValue({ data: { profile: mockResponse } }); const result = await securityHeadersApi.updateProfile(1, updates); expect(client.put).toHaveBeenCalledWith('/security/headers/profiles/1', updates); @@ -55,7 +60,7 @@ describe('securityHeadersApi', () => { }); it('deleteProfile deletes a profile', async () => { - (client.delete as any).mockResolvedValue({}); + mockedDelete.mockResolvedValue({}); await securityHeadersApi.deleteProfile(1); expect(client.delete).toHaveBeenCalledWith('/security/headers/profiles/1'); @@ -63,7 +68,7 @@ describe('securityHeadersApi', () => { it('getPresets returns presets', async () => { const mockPresets = [{ name: 'Basic' }]; - (client.get as any).mockResolvedValue({ data: { presets: mockPresets } }); + mockedGet.mockResolvedValue({ data: { presets: mockPresets } }); const result = await securityHeadersApi.getPresets(); expect(client.get).toHaveBeenCalledWith('/security/headers/presets'); @@ -73,7 +78,7 @@ describe('securityHeadersApi', () => { it('applyPreset applies a preset', async () => { const request = { preset_type: 'basic', name: 'My Preset' }; const mockResponse = { id: 1, ...request }; - (client.post as any).mockResolvedValue({ data: { profile: mockResponse } }); + mockedPost.mockResolvedValue({ data: { profile: mockResponse } }); const result = await securityHeadersApi.applyPreset(request); expect(client.post).toHaveBeenCalledWith('/security/headers/presets/apply', request); @@ -83,7 +88,7 @@ describe('securityHeadersApi', () => { it('calculateScore calculates score', async () => { const config = { hsts_enabled: true }; const mockResponse = { score: 90 }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await securityHeadersApi.calculateScore(config); expect(client.post).toHaveBeenCalledWith('/security/headers/score', config); @@ -93,7 +98,7 @@ describe('securityHeadersApi', () => { it('validateCSP validates CSP', async () => { const csp = "default-src 'self'"; const mockResponse = { valid: true, errors: [] }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await securityHeadersApi.validateCSP(csp); expect(client.post).toHaveBeenCalledWith('/security/headers/csp/validate', { csp }); @@ -103,7 +108,7 @@ describe('securityHeadersApi', () => { it('buildCSP builds CSP', async () => { const directives = [{ directive: 'default-src', values: ["'self'"] }]; const mockResponse = { csp: "default-src 'self'" }; - (client.post as any).mockResolvedValue({ data: mockResponse }); + mockedPost.mockResolvedValue({ data: mockResponse }); const result = await securityHeadersApi.buildCSP(directives); expect(client.post).toHaveBeenCalledWith('/security/headers/csp/build', { directives }); diff --git a/frontend/src/components/AccessListForm.tsx b/frontend/src/components/AccessListForm.tsx index 1612b77c..30b42c80 100644 --- a/frontend/src/components/AccessListForm.tsx +++ b/frontend/src/components/AccessListForm.tsx @@ -378,10 +378,11 @@ export function AccessListForm({ initialData, onSubmit, onCancel, onDelete, isLo
- +

Apply this access list to hosts

setFormData({ ...formData, enabled: checked })} /> @@ -393,12 +394,13 @@ export function AccessListForm({ initialData, onSubmit, onCancel, onDelete, isLo
- +

Allow only private network IPs (10.x.x.x, 192.168.x.x, 172.16-31.x.x)

setFormData({ ...formData, local_network_only: checked }) diff --git a/frontend/src/components/AccessListSelector.tsx b/frontend/src/components/AccessListSelector.tsx index 939004f4..9dcbcdd7 100644 --- a/frontend/src/components/AccessListSelector.tsx +++ b/frontend/src/components/AccessListSelector.tsx @@ -1,5 +1,12 @@ import { useAccessLists } from '../hooks/useAccessLists'; import { ExternalLink } from 'lucide-react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/Select'; interface AccessListSelectorProps { value: number | null; @@ -13,25 +20,28 @@ export default function AccessListSelector({ value, onChange }: AccessListSelect return (
-