chore: enhance coverage for AccessListSelector and ProxyHostForm components
- Added new test suite for AccessListSelector to cover token normalization and emitted values. - Updated existing tests for AccessListSelector to handle prefixed and numeric-string form values. - Introduced tests for ProxyHostForm to validate DNS detection, including error handling and success scenarios. - Enhanced ProxyHostForm tests to cover token normalization for security headers and ensure proper handling of existing host values. - Implemented additional tests for ProxyHostForm to verify domain updates based on selected containers and prompt for new base domains.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import AccessListSelector from '../AccessListSelector';
|
||||
import * as useAccessListsHook from '../../hooks/useAccessLists';
|
||||
|
||||
vi.mock('../../hooks/useAccessLists');
|
||||
|
||||
vi.mock('../ui/Select', () => {
|
||||
const findText = (children: React.ReactNode): string => {
|
||||
if (typeof children === 'string') {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children.map((child) => findText(child)).join(' ');
|
||||
}
|
||||
|
||||
if (children && typeof children === 'object' && 'props' in children) {
|
||||
const node = children as { props?: { children?: React.ReactNode } };
|
||||
return findText(node.props?.children);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const Select = ({ value, onValueChange, children }: { value?: string; onValueChange?: (value: string) => void; children?: React.ReactNode }) => {
|
||||
const text = findText(children);
|
||||
const isAccessList = text.includes('No Access Control (Public)');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isAccessList && (
|
||||
<>
|
||||
<div data-testid="access-list-select-value">{value}</div>
|
||||
<button type="button" onClick={() => onValueChange?.('uuid:acl-uuid-7')}>emit-uuid-token</button>
|
||||
<button type="button" onClick={() => onValueChange?.('123')}>emit-numeric-token</button>
|
||||
<button type="button" onClick={() => onValueChange?.('custom-token')}>emit-custom-token</button>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectTrigger = ({ children, ...rest }: React.ComponentProps<'button'>) => <button type="button" {...rest}>{children}</button>;
|
||||
const SelectContent = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||
const SelectItem = ({ children }: { value: string; children?: React.ReactNode }) => <div>{children}</div>;
|
||||
const SelectValue = ({ placeholder }: { placeholder?: string }) => <span>{placeholder}</span>;
|
||||
|
||||
return {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
};
|
||||
});
|
||||
|
||||
describe('AccessListSelector token coverage branches', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
id: 7,
|
||||
uuid: 'acl-uuid-7',
|
||||
name: 'ACL Seven',
|
||||
description: 'Coverage ACL',
|
||||
type: 'whitelist',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
});
|
||||
|
||||
it('normalizes whitespace and prefixed UUID values in resolver', () => {
|
||||
const onChange = vi.fn();
|
||||
const { rerender } = render(<AccessListSelector value={' '} onChange={onChange} />);
|
||||
|
||||
expect(screen.getByTestId('access-list-select-value')).toHaveTextContent('none');
|
||||
|
||||
rerender(<AccessListSelector value={'uuid:acl-uuid-7'} onChange={onChange} />);
|
||||
expect(screen.getByTestId('access-list-select-value')).toHaveTextContent('id:7');
|
||||
});
|
||||
|
||||
it('maps emitted UUID, numeric, and fallback tokens through handleValueChange', async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<AccessListSelector value={null} onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'emit-uuid-token' }));
|
||||
await user.click(screen.getByRole('button', { name: 'emit-numeric-token' }));
|
||||
await user.click(screen.getByRole('button', { name: 'emit-custom-token' }));
|
||||
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, 7);
|
||||
expect(onChange).toHaveBeenNthCalledWith(2, 123);
|
||||
expect(onChange).toHaveBeenNthCalledWith(3, 'custom-token');
|
||||
});
|
||||
});
|
||||
@@ -231,4 +231,207 @@ describe('AccessListSelector', () => {
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('Hybrid ACL');
|
||||
});
|
||||
|
||||
it('handles prefixed and numeric-string form values as stable selections', () => {
|
||||
const mockLists = [
|
||||
{
|
||||
id: 7,
|
||||
uuid: 'uuid-7',
|
||||
name: 'ACL Seven',
|
||||
description: 'Has both ID and UUID',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: mockLists as unknown as AccessList[],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={'id:7'} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('ACL Seven');
|
||||
|
||||
rerender(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={'7'} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('ACL Seven');
|
||||
});
|
||||
|
||||
it('treats whitespace-only values as no selection', () => {
|
||||
const mockLists = [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'uuid-1',
|
||||
name: 'ACL One',
|
||||
description: 'Baseline ACL',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: mockLists as unknown as AccessList[],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={' '} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('No Access Control (Public)');
|
||||
});
|
||||
|
||||
it('resolves prefixed uuid values to matching id-backed ACL tokens', () => {
|
||||
const mockLists = [
|
||||
{
|
||||
id: 42,
|
||||
uuid: 'acl-uuid-42',
|
||||
name: 'Resolved ACL',
|
||||
description: 'UUID maps to numeric token',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: mockLists as unknown as AccessList[],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
|
||||
const Wrapper = createWrapper();
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={'uuid:acl-uuid-42'} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('Resolved ACL');
|
||||
});
|
||||
|
||||
it('supports UUID-only ACL selection and local-network details', async () => {
|
||||
const uuidOnly = '9f63b8c9-1d26-4b2f-a2c8-001122334455';
|
||||
const mockLists = [
|
||||
{
|
||||
id: undefined,
|
||||
uuid: uuidOnly,
|
||||
name: 'Local UUID ACL',
|
||||
description: 'Only internal network',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: true,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: mockLists as unknown as AccessList[],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
|
||||
const mockOnChange = vi.fn();
|
||||
const Wrapper = createWrapper();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const { rerender } = render(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={null} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: /Access Control List/i }));
|
||||
await user.click(await screen.findByRole('option', { name: 'Local UUID ACL (whitelist)' }));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith(uuidOnly);
|
||||
|
||||
rerender(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={uuidOnly} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Local Network Only \(RFC1918\)/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('skips malformed ACL entries without id or uuid tokens', async () => {
|
||||
const mockLists = [
|
||||
{
|
||||
id: 4,
|
||||
uuid: 'valid-uuid-4',
|
||||
name: 'Valid ACL',
|
||||
description: 'valid option',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
{
|
||||
id: undefined,
|
||||
uuid: undefined,
|
||||
name: 'Malformed ACL',
|
||||
description: 'should be ignored',
|
||||
type: 'whitelist',
|
||||
ip_rules: '[]',
|
||||
country_codes: '',
|
||||
local_network_only: false,
|
||||
enabled: true,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(useAccessListsHook.useAccessLists).mockReturnValue({
|
||||
data: mockLists as unknown as AccessList[],
|
||||
} as unknown as ReturnType<typeof useAccessListsHook.useAccessLists>);
|
||||
|
||||
const mockOnChange = vi.fn();
|
||||
const Wrapper = createWrapper();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<AccessListSelector value={null} onChange={mockOnChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: /Access Control List/i }));
|
||||
|
||||
expect(screen.getByRole('option', { name: 'Valid ACL (whitelist)' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('option', { name: 'Malformed ACL (whitelist)' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import ProxyHostForm from '../ProxyHostForm'
|
||||
import type { ProxyHost } from '../../api/proxyHosts'
|
||||
import { mockRemoteServers } from '../../test/mockData'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
// Mock the hooks
|
||||
vi.mock('../../hooks/useRemoteServers', () => ({
|
||||
@@ -103,6 +104,36 @@ vi.mock('../../hooks/useDNSDetection', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('../DNSDetectionResult', () => ({
|
||||
DNSDetectionResult: ({ result, onUseSuggested, onSelectManually }: {
|
||||
result?: { suggested_provider?: { id: number; name: string } }
|
||||
isLoading: boolean
|
||||
onUseSuggested: (provider: { id: number; name: string }) => void
|
||||
onSelectManually: () => void
|
||||
}) => (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (result?.suggested_provider) {
|
||||
onUseSuggested(result.suggested_provider)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Use Suggested DNS
|
||||
</button>
|
||||
<button type="button" onClick={onSelectManually}>Select Manually DNS</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../api/dnsDetection', () => ({
|
||||
detectDNSProvider: vi.fn().mockResolvedValue({
|
||||
domain: 'example.com',
|
||||
@@ -436,4 +467,139 @@ describe('ProxyHostForm - DNS Provider Integration', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DNS Detection Branches', () => {
|
||||
it('skips detection call when wildcard has provider set and no suggestion', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { useDetectDNSProvider } = await import('../../hooks/useDNSDetection')
|
||||
const detectSpy = vi.fn().mockResolvedValue({
|
||||
domain: 'example.com',
|
||||
detected: false,
|
||||
nameservers: [],
|
||||
confidence: 'none',
|
||||
})
|
||||
|
||||
vi.mocked(useDetectDNSProvider).mockReturnValue({
|
||||
mutateAsync: detectSpy,
|
||||
isPending: false,
|
||||
data: undefined,
|
||||
reset: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDetectDNSProvider>)
|
||||
|
||||
const existingHost: ProxyHost = {
|
||||
uuid: 'test-uuid-skip-detect',
|
||||
name: 'Existing Wildcard Provider',
|
||||
domain_names: '*.example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.100',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
application: 'none',
|
||||
locations: [],
|
||||
enabled: true,
|
||||
dns_provider_id: 1,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
}
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm host={existingHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(600)
|
||||
|
||||
expect(detectSpy).not.toHaveBeenCalled()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('logs detection errors when detectProvider rejects', async () => {
|
||||
const { useDetectDNSProvider } = await import('../../hooks/useDNSDetection')
|
||||
const detectSpy = vi.fn().mockRejectedValue(new Error('detect failed'))
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
vi.mocked(useDetectDNSProvider).mockReturnValue({
|
||||
mutateAsync: detectSpy,
|
||||
isPending: false,
|
||||
data: undefined,
|
||||
reset: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDetectDNSProvider>)
|
||||
|
||||
renderWithClient(<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />)
|
||||
|
||||
const domainInput = screen.getByPlaceholderText('example.com, www.example.com')
|
||||
await userEvent.type(domainInput, '*.example.com')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(errorSpy).toHaveBeenCalledWith('DNS detection failed:', expect.any(Error))
|
||||
})
|
||||
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('auto-selects high confidence suggestion and emits success toast', async () => {
|
||||
const { useDetectDNSProvider } = await import('../../hooks/useDNSDetection')
|
||||
vi.mocked(useDetectDNSProvider).mockReturnValue({
|
||||
mutateAsync: vi.fn().mockResolvedValue({}),
|
||||
isPending: false,
|
||||
data: {
|
||||
domain: 'example.com',
|
||||
detected: true,
|
||||
nameservers: ['ns1.cloudflare.com'],
|
||||
confidence: 'high',
|
||||
suggested_provider: { id: 1, name: 'Cloudflare' },
|
||||
},
|
||||
reset: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDetectDNSProvider>)
|
||||
|
||||
renderWithClient(<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('My Service'), 'Auto Select')
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), '*.example.com')
|
||||
await userEvent.type(screen.getByLabelText(/^Host$/), '192.168.1.100')
|
||||
await userEvent.clear(screen.getByLabelText(/^Port$/))
|
||||
await userEvent.type(screen.getByLabelText(/^Port$/), '8080')
|
||||
await userEvent.click(screen.getByText('Save'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith('Auto-selected: Cloudflare')
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(expect.objectContaining({ dns_provider_id: 1 }))
|
||||
})
|
||||
})
|
||||
|
||||
it('handles suggested and manual selection callbacks from detection result card', async () => {
|
||||
const { useDetectDNSProvider } = await import('../../hooks/useDNSDetection')
|
||||
vi.mocked(useDetectDNSProvider).mockReturnValue({
|
||||
mutateAsync: vi.fn().mockResolvedValue({}),
|
||||
isPending: false,
|
||||
data: {
|
||||
domain: 'example.com',
|
||||
detected: true,
|
||||
nameservers: ['ns1.cloudflare.com'],
|
||||
confidence: 'medium',
|
||||
suggested_provider: { id: 1, name: 'Cloudflare' },
|
||||
},
|
||||
reset: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDetectDNSProvider>)
|
||||
|
||||
renderWithClient(<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), '*.example.com')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Use Suggested DNS' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use Suggested DNS' }))
|
||||
expect(toast.success).toHaveBeenCalledWith('Selected: Cloudflare')
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Select Manually DNS' }))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -663,4 +663,147 @@ describe('ProxyHostForm Dropdown Change Bug Fix', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('initializes edit mode from nested ACL and security header UUID references', async () => {
|
||||
const user = userEvent.setup()
|
||||
const Wrapper = createWrapper()
|
||||
|
||||
const existingHost = {
|
||||
uuid: 'host-uuid-nested-ref',
|
||||
name: 'Nested Ref Host',
|
||||
domain_names: 'test.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: true,
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
enable_standard_headers: true,
|
||||
application: 'none',
|
||||
advanced_config: '',
|
||||
enabled: true,
|
||||
locations: [],
|
||||
certificate_id: null,
|
||||
access_list_id: null,
|
||||
security_header_profile_id: null,
|
||||
access_list: { uuid: 'acl-uuid-2' },
|
||||
security_header_profile: { uuid: 'profile-uuid-2' },
|
||||
dns_provider_id: null,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
} as unknown as ProxyHost
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ProxyHostForm host={existingHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('VPN Users')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
access_list_id: 'acl-uuid-2',
|
||||
security_header_profile_id: 'profile-uuid-2',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes empty and numeric-string ACL/security references on submit', async () => {
|
||||
const user = userEvent.setup()
|
||||
const Wrapper = createWrapper()
|
||||
|
||||
const hostWithStringReferences = {
|
||||
uuid: 'host-uuid-string-refs',
|
||||
name: 'String Ref Host',
|
||||
domain_names: 'test.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: true,
|
||||
block_exploits: true,
|
||||
websocket_support: false,
|
||||
enable_standard_headers: true,
|
||||
application: 'none',
|
||||
advanced_config: '',
|
||||
enabled: true,
|
||||
locations: [],
|
||||
certificate_id: null,
|
||||
access_list_id: '2',
|
||||
security_header_profile_id: ' ',
|
||||
dns_provider_id: null,
|
||||
created_at: '2024-01-01',
|
||||
updated_at: '2024-01-01',
|
||||
} as unknown as ProxyHost
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ProxyHostForm host={hostWithStringReferences} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Access Control List/i })).toHaveTextContent('VPN Users')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
access_list_id: 2,
|
||||
security_header_profile_id: null,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('filters out security profiles missing both id and uuid', async () => {
|
||||
const user = userEvent.setup()
|
||||
const Wrapper = createWrapper()
|
||||
|
||||
vi.mocked(useSecurityHeaderProfiles).mockReturnValue({
|
||||
data: [
|
||||
{
|
||||
...mockSecurityProfiles[0],
|
||||
id: undefined,
|
||||
uuid: undefined,
|
||||
name: 'Broken Profile',
|
||||
},
|
||||
{
|
||||
...mockSecurityProfiles[1],
|
||||
id: 2,
|
||||
uuid: 'profile-uuid-2',
|
||||
name: 'Strict Security',
|
||||
},
|
||||
] as unknown as SecurityHeaderProfile[],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as unknown as ReturnType<typeof useSecurityHeaderProfiles>)
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
await user.type(screen.getByLabelText(/^Name/), 'Filter Profile Host')
|
||||
await user.type(screen.getByLabelText(/Domain Names/), 'test.com')
|
||||
await user.type(screen.getByLabelText(/^Host$/), 'localhost')
|
||||
await user.clear(screen.getByLabelText(/^Port$/))
|
||||
await user.type(screen.getByLabelText(/^Port$/), '8080')
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: /Security Headers/i }))
|
||||
|
||||
expect(screen.queryByRole('option', { name: /Broken Profile/i })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('option', { name: /Strict Security/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import ProxyHostForm from '../ProxyHostForm';
|
||||
import type { ProxyHost } from '../../api/proxyHosts';
|
||||
|
||||
vi.mock('../../hooks/useRemoteServers', () => ({
|
||||
useRemoteServers: vi.fn(() => ({
|
||||
servers: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useDocker', () => ({
|
||||
useDocker: vi.fn(() => ({
|
||||
containers: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useDomains', () => ({
|
||||
useDomains: vi.fn(() => ({
|
||||
domains: [{ uuid: 'domain-1', name: 'test.com' }],
|
||||
createDomain: vi.fn().mockResolvedValue({}),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useCertificates', () => ({
|
||||
useCertificates: vi.fn(() => ({
|
||||
certificates: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useDNSDetection', () => ({
|
||||
useDetectDNSProvider: vi.fn(() => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
data: undefined,
|
||||
reset: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useAccessLists', () => ({
|
||||
useAccessLists: vi.fn(() => ({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'acl-uuid-1',
|
||||
name: 'Office Network',
|
||||
description: 'Office IP range',
|
||||
type: 'whitelist',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useSecurityHeaders', () => ({
|
||||
useSecurityHeaderProfiles: vi.fn(() => ({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
uuid: 'profile-uuid-1',
|
||||
name: 'Basic Security',
|
||||
description: 'Basic security headers',
|
||||
is_preset: true,
|
||||
preset_type: 'basic',
|
||||
security_score: 60,
|
||||
},
|
||||
{
|
||||
id: undefined,
|
||||
uuid: undefined,
|
||||
name: 'Malformed Custom',
|
||||
description: 'Should be skipped in options map',
|
||||
is_preset: false,
|
||||
preset_type: 'custom',
|
||||
security_score: 10,
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../ui/Select', () => {
|
||||
const findText = (children: React.ReactNode): string => {
|
||||
if (typeof children === 'string') {
|
||||
return children;
|
||||
}
|
||||
|
||||
if (Array.isArray(children)) {
|
||||
return children.map((child) => findText(child)).join(' ');
|
||||
}
|
||||
|
||||
if (children && typeof children === 'object' && 'props' in children) {
|
||||
const node = children as { props?: { children?: React.ReactNode } };
|
||||
return findText(node.props?.children);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const Select = ({ value, onValueChange, children }: { value?: string; onValueChange?: (value: string) => void; children?: React.ReactNode }) => {
|
||||
const text = findText(children);
|
||||
const isSecurityHeaders = text.includes('None (No Security Headers)');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isSecurityHeaders && (
|
||||
<>
|
||||
<div data-testid="security-select-value">{value}</div>
|
||||
<button type="button" onClick={() => onValueChange?.('42')}>emit-security-plain-numeric</button>
|
||||
<button type="button" onClick={() => onValueChange?.('custom-header-token')}>emit-security-custom</button>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectTrigger = ({ children, ...rest }: React.ComponentProps<'button'>) => <button type="button" {...rest}>{children}</button>;
|
||||
const SelectContent = ({ children }: { children?: React.ReactNode }) => <div>{children}</div>;
|
||||
const SelectItem = ({ children }: { value: string; children?: React.ReactNode }) => <div>{children}</div>;
|
||||
const SelectValue = () => <span />;
|
||||
|
||||
return {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
};
|
||||
});
|
||||
|
||||
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ internal_ip: '127.0.0.1' }) })));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const fillRequiredFields = async () => {
|
||||
await userEvent.type(screen.getByLabelText(/^Name/), 'Coverage Host');
|
||||
await userEvent.type(screen.getByLabelText(/Domain Names/), 'test.com');
|
||||
await userEvent.type(screen.getByLabelText(/^Host$/), 'localhost');
|
||||
await userEvent.clear(screen.getByLabelText(/^Port$/));
|
||||
await userEvent.type(screen.getByLabelText(/^Port$/), '8080');
|
||||
};
|
||||
|
||||
describe('ProxyHostForm token coverage branches', () => {
|
||||
const onCancel = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('normalizes prefixed and numeric-string security header IDs', async () => {
|
||||
const onSubmit = vi.fn<(data: Partial<ProxyHost>) => Promise<void>>().mockResolvedValue();
|
||||
const Wrapper = createWrapper();
|
||||
|
||||
const { rerender } = render(
|
||||
<Wrapper>
|
||||
<ProxyHostForm
|
||||
host={{
|
||||
uuid: 'host-1',
|
||||
domain_names: 'a.test',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
security_header_profile_id: 'id:7',
|
||||
} as ProxyHost}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('security-select-value')).toHaveTextContent('id:7');
|
||||
|
||||
rerender(
|
||||
<Wrapper>
|
||||
<ProxyHostForm
|
||||
host={{
|
||||
uuid: 'host-2',
|
||||
domain_names: 'b.test',
|
||||
forward_host: 'localhost',
|
||||
forward_port: 8080,
|
||||
security_header_profile_id: '12',
|
||||
} as ProxyHost}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('security-select-value')).toHaveTextContent('id:12');
|
||||
});
|
||||
|
||||
it('converts plain numeric and custom security tokens on submit', async () => {
|
||||
const onSubmit = vi.fn<(data: Partial<ProxyHost>) => Promise<void>>().mockResolvedValue();
|
||||
const Wrapper = createWrapper();
|
||||
|
||||
render(
|
||||
<Wrapper>
|
||||
<ProxyHostForm onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
await fillRequiredFields();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'emit-security-plain-numeric' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /Save/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ security_header_profile_id: 42 })
|
||||
);
|
||||
});
|
||||
|
||||
onSubmit.mockClear();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'emit-security-custom' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: /Save/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ security_header_profile_id: 'custom-header-token' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,4 +109,39 @@ describe('ProxyHostForm Add Uptime flow', () => {
|
||||
expect(submittedPayload).not.toHaveProperty('uptimeInterval')
|
||||
expect(submittedPayload).not.toHaveProperty('uptimeMaxRetries')
|
||||
})
|
||||
|
||||
it('shows uptime sync fallback error toast when monitor request fails with empty string error', async () => {
|
||||
const onSubmit = vi.fn(() => Promise.resolve())
|
||||
const onCancel = vi.fn()
|
||||
|
||||
const uptime = await import('../../api/uptime')
|
||||
const syncMock = uptime.syncMonitors as unknown as import('vitest').Mock
|
||||
syncMock.mockRejectedValueOnce('')
|
||||
|
||||
const toastModule = await import('react-hot-toast')
|
||||
const errorSpy = vi.spyOn(toastModule.toast, 'error')
|
||||
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ProxyHostForm onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('My Service'), 'My Service')
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'example.com')
|
||||
await userEvent.type(screen.getByLabelText(/^Host$/), '127.0.0.1')
|
||||
await userEvent.clear(screen.getByLabelText(/^Port$/))
|
||||
await userEvent.type(screen.getByLabelText(/^Port$/), '8080')
|
||||
|
||||
await userEvent.click(screen.getByLabelText(/Add Uptime monitoring for this host/i))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled()
|
||||
expect(syncMock).toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalledWith('Failed to request uptime creation')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,6 +123,13 @@ vi.mock('../../api/proxyHosts', () => ({
|
||||
testProxyHostConnection: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock global fetch for health API
|
||||
const mockFetch = vi.fn()
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
@@ -552,6 +559,51 @@ describe('ProxyHostForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('closes preset overwrite modal when cancel is clicked', async () => {
|
||||
const existingHost = {
|
||||
uuid: 'test-uuid',
|
||||
name: 'CancelOverwrite',
|
||||
domain_names: 'test.example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: '192.168.1.2',
|
||||
forward_port: 8080,
|
||||
advanced_config: '{"handler":"headers","request":{"set":{"X-Test":"value"}}}',
|
||||
advanced_config_backup: '',
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: false,
|
||||
block_exploits: true,
|
||||
websocket_support: true,
|
||||
application: 'none' as const,
|
||||
locations: [],
|
||||
enabled: true,
|
||||
created_at: '2025-01-01',
|
||||
updated_at: '2025-01-01',
|
||||
}
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm host={existingHost as ProxyHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Confirm Preset Overwrite')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const modal = screen.getByText('Confirm Preset Overwrite').closest('div')?.parentElement
|
||||
if (!modal) {
|
||||
throw new Error('Preset overwrite modal not found')
|
||||
}
|
||||
|
||||
await userEvent.click(within(modal).getByRole('button', { name: 'Cancel' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Confirm Preset Overwrite')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('restores previous advanced_config from backup when clicking restore', async () => {
|
||||
const existingHost = {
|
||||
uuid: 'test-uuid',
|
||||
@@ -700,6 +752,83 @@ describe('ProxyHostForm', () => {
|
||||
expect(screen.getByText('Copied!')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('copies plex trusted proxy IP helper snippet', async () => {
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: mockWriteText },
|
||||
})
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'apps.mydomain.com')
|
||||
|
||||
await selectComboboxOption(/Application Preset/i, 'Plex - Media server with remote access')
|
||||
await userEvent.click(screen.getAllByRole('button', { name: /Copy/i })[1])
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('192.168.1.50')
|
||||
})
|
||||
})
|
||||
|
||||
it('copies jellyfin trusted proxy IP helper snippet', async () => {
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: mockWriteText },
|
||||
})
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'apps.mydomain.com')
|
||||
await selectComboboxOption(/Application Preset/i, 'Jellyfin - Open source media server')
|
||||
await userEvent.click(screen.getByRole('button', { name: /Copy/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('192.168.1.50')
|
||||
})
|
||||
})
|
||||
|
||||
it('copies home assistant helper yaml snippet', async () => {
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: mockWriteText },
|
||||
})
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'apps.mydomain.com')
|
||||
await selectComboboxOption(/Application Preset/i, 'Home Assistant - Home automation')
|
||||
await userEvent.click(screen.getByRole('button', { name: /Copy/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith('http:\n use_x_forwarded_for: true\n trusted_proxies:\n - 192.168.1.50')
|
||||
})
|
||||
})
|
||||
|
||||
it('copies nextcloud helper php snippet', async () => {
|
||||
const mockWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: mockWriteText },
|
||||
})
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('example.com, www.example.com'), 'apps.mydomain.com')
|
||||
await selectComboboxOption(/Application Preset/i, 'Nextcloud - File sync and share')
|
||||
await userEvent.click(screen.getByRole('button', { name: /Copy/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockWriteText).toHaveBeenCalledWith("'trusted_proxies' => ['192.168.1.50'],\n'overwriteprotocol' => 'https',")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Security Options', () => {
|
||||
@@ -943,6 +1072,85 @@ describe('ProxyHostForm', () => {
|
||||
await selectComboboxOption(/Security Headers/i, 'Custom Profile (Score: 70/100)')
|
||||
expect(screen.getByRole('combobox', { name: /Security Headers/i })).toHaveTextContent('Custom Profile')
|
||||
})
|
||||
|
||||
it('resolves prefixed security header id tokens from existing host values', async () => {
|
||||
const existingHost = {
|
||||
uuid: 'security-token-host',
|
||||
name: 'Token Host',
|
||||
domain_names: 'token.example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: '127.0.0.1',
|
||||
forward_port: 80,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: true,
|
||||
block_exploits: true,
|
||||
websocket_support: true,
|
||||
application: 'none' as const,
|
||||
locations: [],
|
||||
enabled: true,
|
||||
security_header_profile_id: 'id:100',
|
||||
created_at: '2025-01-01',
|
||||
updated_at: '2025-01-01',
|
||||
}
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm host={existingHost as unknown as ProxyHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Security Headers/i })).toHaveTextContent('Strict Profile')
|
||||
})
|
||||
|
||||
it('resolves numeric-string security header ids from existing host values', async () => {
|
||||
const existingHost = {
|
||||
uuid: 'security-numeric-host',
|
||||
name: 'Numeric Host',
|
||||
domain_names: 'numeric.example.com',
|
||||
forward_scheme: 'http',
|
||||
forward_host: '127.0.0.1',
|
||||
forward_port: 80,
|
||||
ssl_forced: true,
|
||||
http2_support: true,
|
||||
hsts_enabled: true,
|
||||
hsts_subdomains: true,
|
||||
block_exploits: true,
|
||||
websocket_support: true,
|
||||
application: 'none' as const,
|
||||
locations: [],
|
||||
enabled: true,
|
||||
security_header_profile_id: '100',
|
||||
created_at: '2025-01-01',
|
||||
updated_at: '2025-01-01',
|
||||
}
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm host={existingHost as unknown as ProxyHost} onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
expect(screen.getByRole('combobox', { name: /Security Headers/i })).toHaveTextContent('Strict Profile')
|
||||
})
|
||||
|
||||
it('skips non-preset profiles that have neither id nor uuid', async () => {
|
||||
const { useSecurityHeaderProfiles } = await import('../../hooks/useSecurityHeaders')
|
||||
vi.mocked(useSecurityHeaderProfiles).mockReturnValue({
|
||||
data: [
|
||||
{ id: 100, name: 'Strict Profile', description: 'Very strict', security_score: 90, is_preset: true, preset_type: 'strict' },
|
||||
{ name: 'Invalid Custom', description: 'No identity token', security_score: 10, is_preset: false },
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as unknown as ReturnType<typeof useSecurityHeaderProfiles>)
|
||||
|
||||
renderWithClient(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await userEvent.click(screen.getByRole('combobox', { name: /Security Headers/i }))
|
||||
|
||||
expect(screen.queryByRole('option', { name: /Invalid Custom/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('Edit Mode vs Create Mode', () => {
|
||||
@@ -1247,6 +1455,55 @@ describe('ProxyHostForm', () => {
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('updates domain using selected container when base domain changes', async () => {
|
||||
const { useDocker } = await import('../../hooks/useDocker')
|
||||
vi.mocked(useDocker).mockReturnValue({
|
||||
containers: [
|
||||
{
|
||||
id: 'container-123',
|
||||
names: ['my-app'],
|
||||
image: 'nginx:latest',
|
||||
state: 'running',
|
||||
status: 'Up 2 hours',
|
||||
network: 'bridge',
|
||||
ip: '172.17.0.2',
|
||||
ports: [{ private_port: 80, public_port: 8080, type: 'tcp' }],
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
await renderWithClientAct(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
await selectComboboxOption('Source', 'Local (Docker Socket)')
|
||||
await selectComboboxOption('Containers', 'my-app (nginx:latest)')
|
||||
await selectComboboxOption(/Base Domain/i, 'existing.com')
|
||||
|
||||
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('my-app.existing.com')
|
||||
})
|
||||
|
||||
it('prompts to save a new base domain when user enters a base domain directly', async () => {
|
||||
localStorage.removeItem('charon_dont_ask_domain')
|
||||
localStorage.removeItem('cpmp_dont_ask_domain')
|
||||
|
||||
await renderWithClientAct(
|
||||
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
|
||||
)
|
||||
|
||||
const domainInput = screen.getByPlaceholderText('example.com, www.example.com')
|
||||
await userEvent.type(domainInput, 'brandnewdomain.com')
|
||||
await userEvent.tab()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('New Base Domain Detected')).toBeInTheDocument()
|
||||
expect(screen.getByText('brandnewdomain.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Host and Port Combination', () => {
|
||||
|
||||
Reference in New Issue
Block a user