chore: refactor tests to improve clarity and reliability
- Removed unnecessary test.skip() calls in various test files, replacing them with comments for clarity. - Enhanced retry logic in TestDataManager for API requests to handle rate limiting more gracefully. - Updated security helper functions to include retry mechanisms for fetching security status and setting module states. - Improved loading completion checks to handle page closure scenarios. - Adjusted WebKit-specific tests to run in all browsers, removing the previous skip logic. - General cleanup and refactoring across multiple test files to enhance readability and maintainability.
This commit is contained in:
@@ -18,6 +18,13 @@ import DNSProviderSelector from './DNSProviderSelector'
|
||||
import { useDetectDNSProvider } from '../hooks/useDNSDetection'
|
||||
import { DNSDetectionResult } from './DNSDetectionResult'
|
||||
import type { DNSProvider } from '../api/dnsProviders'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from './ui/Select'
|
||||
|
||||
// Application preset configurations
|
||||
const APPLICATION_PRESETS: { value: ApplicationPreset; label: string; description: string }[] = [
|
||||
@@ -126,6 +133,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
const { mutateAsync: detectProvider, isPending: isDetecting, data: detectionResult, reset: resetDetection } = useDetectDNSProvider()
|
||||
const [manualProviderSelection, setManualProviderSelection] = useState(false)
|
||||
const detectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const portInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
// Fetch Charon internal IP on mount (legacy: CPMP internal IP)
|
||||
useEffect(() => {
|
||||
@@ -384,6 +392,20 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.forward_port) {
|
||||
portInputRef.current?.setCustomValidity('Port is required')
|
||||
portInputRef.current?.reportValidity()
|
||||
portInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.forward_port < 1 || formData.forward_port > 65535) {
|
||||
portInputRef.current?.setCustomValidity('Port must be between 1 and 65535')
|
||||
portInputRef.current?.reportValidity()
|
||||
portInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate DNS provider for wildcard domains
|
||||
if (hasWildcardDomain && !formData.dns_provider_id) {
|
||||
toast.error('DNS provider is required for wildcard domains')
|
||||
@@ -571,50 +593,51 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Docker Container Quick Select */}
|
||||
<div>
|
||||
<label htmlFor="connection-source" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Source
|
||||
</label>
|
||||
<select
|
||||
id="connection-source"
|
||||
value={connectionSource}
|
||||
onChange={e => setConnectionSource(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="custom">Custom / Manual</option>
|
||||
<option value="local">Local (Docker Socket)</option>
|
||||
{remoteServers
|
||||
.filter(s => s.provider === 'docker' && s.enabled)
|
||||
.map(server => (
|
||||
<option key={server.uuid} value={server.uuid}>
|
||||
{server.name} ({server.host})
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
<Select value={connectionSource} onValueChange={setConnectionSource}>
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="Source">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="custom">Custom / Manual</SelectItem>
|
||||
<SelectItem value="local">Local (Docker Socket)</SelectItem>
|
||||
{remoteServers
|
||||
.filter(s => s.provider === 'docker' && s.enabled)
|
||||
.map(server => (
|
||||
<SelectItem key={server.uuid} value={server.uuid}>
|
||||
{server.name} ({server.host})
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="quick-select-docker" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Containers
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="quick-select-docker"
|
||||
onChange={e => handleContainerSelect(e.target.value)}
|
||||
disabled={dockerLoading || connectionSource === 'custom'}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={e => e && handleContainerSelect(e)}
|
||||
>
|
||||
<option value="">
|
||||
{connectionSource === 'custom'
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white disabled:opacity-50" disabled={dockerLoading || connectionSource === 'custom'} aria-label="Containers">
|
||||
<SelectValue placeholder={connectionSource === 'custom'
|
||||
? 'Select a source to view containers'
|
||||
: (dockerLoading ? 'Loading containers...' : '-- Select a container --')}
|
||||
</option>
|
||||
{dockerContainers.map(container => (
|
||||
<option key={container.id} value={container.id}>
|
||||
{container.names[0]} ({container.image})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
: (dockerLoading ? 'Loading containers...' : 'Select a container')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dockerContainers.map(container => (
|
||||
<SelectItem key={container.id} value={container.id}>
|
||||
{container.names[0]} ({container.image})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{dockerError && connectionSource !== 'custom' && (
|
||||
<div className="mt-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -639,22 +662,21 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
<div className="space-y-4">
|
||||
{domains.length > 0 && (
|
||||
<div>
|
||||
<label htmlFor="base-domain" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Base Domain (Auto-fill)
|
||||
</label>
|
||||
<select
|
||||
id="base-domain"
|
||||
value={selectedDomain}
|
||||
onChange={e => handleBaseDomainChange(e.target.value)}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">-- Select a base domain --</option>
|
||||
{domains.map(domain => (
|
||||
<option key={domain.uuid} value={domain.name}>
|
||||
{domain.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={selectedDomain} onValueChange={handleBaseDomainChange}>
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="Base Domain (Auto-fill)">
|
||||
<SelectValue placeholder="Select a base domain" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{domains.map(domain => (
|
||||
<SelectItem key={domain.uuid} value={domain.name}>
|
||||
{domain.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
@@ -677,16 +699,16 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
{/* Forward Details */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label htmlFor="forward-scheme" className="block text-sm font-medium text-gray-300 mb-2">Scheme</label>
|
||||
<select
|
||||
id="forward-scheme"
|
||||
value={formData.forward_scheme}
|
||||
onChange={e => setFormData({ ...formData, forward_scheme: e.target.value })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Scheme</label>
|
||||
<Select value={formData.forward_scheme} onValueChange={scheme => setFormData({ ...formData, forward_scheme: scheme })}>
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="Scheme">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">HTTP</SelectItem>
|
||||
<SelectItem value="https">HTTPS</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="forward-host" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
@@ -721,9 +743,11 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
required
|
||||
min="1"
|
||||
max="65535"
|
||||
ref={portInputRef}
|
||||
value={formData.forward_port}
|
||||
onChange={e => {
|
||||
const v = parseInt(e.target.value)
|
||||
portInputRef.current?.setCustomValidity('')
|
||||
setFormData({ ...formData, forward_port: Number.isNaN(v) ? 0 : v })
|
||||
}}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -736,19 +760,20 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
SSL Certificate
|
||||
</label>
|
||||
<select
|
||||
value={formData.certificate_id || 0}
|
||||
onChange={e => setFormData({ ...formData, certificate_id: parseInt(e.target.value) || null })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={0}>Auto-manage with Let's Encrypt (recommended)</option>
|
||||
{certificates.map(cert => (
|
||||
<option key={cert.id || cert.domain} value={cert.id ?? 0}>
|
||||
{(cert.name || cert.domain)}
|
||||
{cert.provider ? ` (${cert.provider})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select value={String(formData.certificate_id || 0)} onValueChange={e => setFormData({ ...formData, certificate_id: parseInt(e) || null })}>
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="SSL Certificate">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">Auto-manage with Let's Encrypt (recommended)</SelectItem>
|
||||
{certificates.map(cert => (
|
||||
<SelectItem key={cert.id || cert.domain} value={String(cert.id ?? 0)}>
|
||||
{(cert.name || cert.domain)}
|
||||
{cert.provider ? ` (${cert.provider})` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Choose an existing certificate if already issued for these domains, or let Charon request/renew via Let's Encrypt automatically.
|
||||
</p>
|
||||
@@ -804,37 +829,39 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
<span className="text-gray-500 font-normal ml-2">(Optional)</span>
|
||||
</label>
|
||||
|
||||
<select
|
||||
value={formData.security_header_profile_id || 0}
|
||||
onChange={e => {
|
||||
const value = e.target.value === "0" ? null : parseInt(e.target.value) || null
|
||||
<Select
|
||||
value={String(formData.security_header_profile_id || 0)}
|
||||
onValueChange={e => {
|
||||
const value = e === "0" ? null : parseInt(e) || null
|
||||
setFormData({ ...formData, security_header_profile_id: value })
|
||||
}}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={0}>None (No Security Headers)</option>
|
||||
<optgroup label="Quick Presets">
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="Security Headers">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">None (No Security Headers)</SelectItem>
|
||||
{securityProfiles
|
||||
?.filter(p => p.is_preset)
|
||||
.sort((a, b) => a.security_score - b.security_score)
|
||||
.map(profile => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
<SelectItem key={profile.id} value={String(profile.id)}>
|
||||
{profile.name} (Score: {profile.security_score}/100)
|
||||
</option>
|
||||
</SelectItem>
|
||||
))}
|
||||
</optgroup>
|
||||
{(securityProfiles?.filter(p => !p.is_preset) || []).length > 0 && (
|
||||
<optgroup label="Custom Profiles">
|
||||
{(securityProfiles || [])
|
||||
.filter(p => !p.is_preset)
|
||||
.map(profile => (
|
||||
<option key={profile.id} value={profile.id}>
|
||||
{profile.name} (Score: {profile.security_score}/100)
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
{(securityProfiles?.filter(p => !p.is_preset) || []).length > 0 && (
|
||||
<>
|
||||
{(securityProfiles || [])
|
||||
.filter(p => !p.is_preset)
|
||||
.map(profile => (
|
||||
<SelectItem key={profile.id} value={String(profile.id)}>
|
||||
{profile.name} (Score: {profile.security_score}/100)
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{formData.security_header_profile_id && (() => {
|
||||
const selected = securityProfiles?.find(p => p.id === formData.security_header_profile_id)
|
||||
@@ -893,30 +920,33 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
|
||||
{/* Application Preset */}
|
||||
<div>
|
||||
<label htmlFor="application-preset" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Application Preset
|
||||
<span className="text-gray-500 font-normal ml-2">(Optional)</span>
|
||||
</label>
|
||||
<select
|
||||
id="application-preset"
|
||||
<Select
|
||||
value={formData.application}
|
||||
onChange={e => {
|
||||
const preset = e.target.value as ApplicationPreset
|
||||
onValueChange={preset => {
|
||||
const presetVal = preset as ApplicationPreset
|
||||
// Apply with advanced_config logic
|
||||
const needsWebsockets = ['plex', 'jellyfin', 'emby', 'homeassistant', 'vaultwarden'].includes(preset)
|
||||
const needsWebsockets = ['plex', 'jellyfin', 'emby', 'homeassistant', 'vaultwarden'].includes(presetVal)
|
||||
// Delegate to shared logic which will auto-apply or prompt
|
||||
tryApplyPreset(preset)
|
||||
tryApplyPreset(presetVal)
|
||||
// Ensure we still enable websockets when preset implies it
|
||||
setFormData(prev => ({ ...prev, websocket_support: needsWebsockets || prev.websocket_support }))
|
||||
}}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{APPLICATION_PRESETS.map(preset => (
|
||||
<option key={preset.value} value={preset.value}>
|
||||
{preset.label} - {preset.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectTrigger className="w-full bg-gray-900 border-gray-700 text-white" aria-label="Application Preset">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{APPLICATION_PRESETS.map(preset => (
|
||||
<SelectItem key={preset.value} value={preset.value}>
|
||||
{preset.label} - {preset.description}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Presets automatically configure headers for remote access behind tunnels/CGNAT.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user