feat: add L4 (TCP/UDP) proxy host support via caddy-l4
- New l4_proxy_hosts table and Drizzle migration (0015) - Full CRUD model layer with validation, audit logging, and Caddy config generation (buildL4Servers integrating into buildCaddyDocument) - Server actions, paginated list page, create/edit/delete dialogs - L4 port manager sidecar (docker/l4-port-manager) that auto-recreates the caddy container when port mappings change via a trigger file - Auto-detects Docker Compose project name from caddy container labels - Supports both named-volume and bind-mount (COMPOSE_HOST_DIR) deployments - getL4PortsStatus simplified: status file is sole source of truth, trigger files deleted after processing to prevent stuck 'Waiting' banner - Navigation entry added (CableIcon) - Tests: unit (entrypoint.sh invariants + validation), integration (ports lifecycle + caddy config), E2E (CRUD + functional routing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
80
tests/helpers/l4-proxy-api.ts
Normal file
80
tests/helpers/l4-proxy-api.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Higher-level helpers for creating L4 proxy hosts in E2E tests.
|
||||
*
|
||||
* All helpers accept a Playwright `Page` (pre-authenticated via the
|
||||
* global storageState) so they integrate cleanly with the standard
|
||||
* `page` test fixture.
|
||||
*/
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export interface L4ProxyHostConfig {
|
||||
name: string;
|
||||
protocol?: 'tcp' | 'udp';
|
||||
listenAddress: string;
|
||||
upstream: string; // e.g. "tcp-echo:9000"
|
||||
matcherType?: 'none' | 'tls_sni' | 'http_host' | 'proxy_protocol';
|
||||
matcherValue?: string; // comma-separated
|
||||
tlsTermination?: boolean;
|
||||
proxyProtocolReceive?: boolean;
|
||||
proxyProtocolVersion?: 'v1' | 'v2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an L4 proxy host via the browser UI.
|
||||
*/
|
||||
export async function createL4ProxyHost(page: Page, config: L4ProxyHostConfig): Promise<void> {
|
||||
await page.goto('/l4-proxy-hosts');
|
||||
await page.getByRole('button', { name: /create l4 host/i }).click();
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Name').fill(config.name);
|
||||
|
||||
// Protocol select
|
||||
if (config.protocol && config.protocol !== 'tcp') {
|
||||
await page.getByLabel('Protocol').click();
|
||||
await page.getByRole('option', { name: new RegExp(config.protocol, 'i') }).click();
|
||||
}
|
||||
|
||||
await page.getByLabel('Listen Address').fill(config.listenAddress);
|
||||
await page.getByLabel('Upstreams').fill(config.upstream);
|
||||
|
||||
// Matcher type
|
||||
if (config.matcherType && config.matcherType !== 'none') {
|
||||
await page.getByLabel('Matcher').click();
|
||||
const matcherLabels: Record<string, RegExp> = {
|
||||
tls_sni: /tls sni/i,
|
||||
http_host: /http host/i,
|
||||
proxy_protocol: /proxy protocol/i,
|
||||
};
|
||||
await page.getByRole('option', { name: matcherLabels[config.matcherType] }).click();
|
||||
|
||||
if (config.matcherValue && (config.matcherType === 'tls_sni' || config.matcherType === 'http_host')) {
|
||||
await page.getByLabel(/hostnames/i).fill(config.matcherValue);
|
||||
}
|
||||
}
|
||||
|
||||
// TLS termination
|
||||
if (config.tlsTermination) {
|
||||
await page.getByLabel(/tls termination/i).check();
|
||||
}
|
||||
|
||||
// Proxy protocol receive
|
||||
if (config.proxyProtocolReceive) {
|
||||
await page.getByLabel(/accept inbound proxy/i).check();
|
||||
}
|
||||
|
||||
// Proxy protocol version
|
||||
if (config.proxyProtocolVersion) {
|
||||
await page.getByLabel(/send proxy protocol/i).click();
|
||||
await page.getByRole('option', { name: config.proxyProtocolVersion }).click();
|
||||
}
|
||||
|
||||
// Submit
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
// Wait for success state (dialog closes or success alert)
|
||||
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Verify host appears in the table
|
||||
await expect(page.getByText(config.name)).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
Reference in New Issue
Block a user