Files
caddy-proxy-manager/tests/integration/l4-proxy-hosts.test.ts
akanealw 99819b70ff
Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
added caddy-proxy-manager for testing
2026-04-21 22:49:08 +00:00

336 lines
14 KiB
TypeScript
Executable File

import { describe, it, expect, beforeEach } from 'vitest';
import { createTestDb, type TestDb } from '../helpers/db';
import { l4ProxyHosts } from '../../src/lib/db/schema';
import { eq } from 'drizzle-orm';
let db: TestDb;
beforeEach(() => {
db = createTestDb();
});
function nowIso() {
return new Date().toISOString();
}
async function insertL4Host(overrides: Partial<typeof l4ProxyHosts.$inferInsert> = {}) {
const now = nowIso();
const [host] = await db.insert(l4ProxyHosts).values({
name: 'Test L4 Host',
protocol: 'tcp',
listenAddress: ':5432',
upstreams: JSON.stringify(['10.0.0.1:5432']),
matcherType: 'none',
matcherValue: null,
tlsTermination: false,
proxyProtocolVersion: null,
proxyProtocolReceive: false,
enabled: true,
createdAt: now,
updatedAt: now,
...overrides,
}).returning();
return host;
}
// ---------------------------------------------------------------------------
// Basic CRUD
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts integration', () => {
it('inserts and retrieves an L4 proxy host', async () => {
const host = await insertL4Host();
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row).toBeDefined();
expect(row!.name).toBe('Test L4 Host');
expect(row!.protocol).toBe('tcp');
expect(row!.listenAddress).toBe(':5432');
});
it('delete by id removes the host', async () => {
const host = await insertL4Host();
await db.delete(l4ProxyHosts).where(eq(l4ProxyHosts.id, host.id));
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row).toBeUndefined();
});
it('multiple L4 hosts — count is correct', async () => {
await insertL4Host({ name: 'PG', listenAddress: ':5432' });
await insertL4Host({ name: 'MySQL', listenAddress: ':3306' });
await insertL4Host({ name: 'Redis', listenAddress: ':6379' });
const rows = await db.select().from(l4ProxyHosts);
expect(rows.length).toBe(3);
});
it('enabled field defaults to true', async () => {
const host = await insertL4Host();
expect(host.enabled).toBe(true);
});
it('can set enabled to false', async () => {
const host = await insertL4Host({ enabled: false });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.enabled)).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Protocol field
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts protocol', () => {
it('stores TCP protocol', async () => {
const host = await insertL4Host({ protocol: 'tcp' });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.protocol).toBe('tcp');
});
it('stores UDP protocol', async () => {
const host = await insertL4Host({ protocol: 'udp' });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.protocol).toBe('udp');
});
});
// ---------------------------------------------------------------------------
// JSON fields (upstreams, matcher_value)
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts JSON fields', () => {
it('stores and retrieves upstreams array', async () => {
const upstreams = ['10.0.0.1:5432', '10.0.0.2:5432', '10.0.0.3:5432'];
const host = await insertL4Host({ upstreams: JSON.stringify(upstreams) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(JSON.parse(row!.upstreams)).toEqual(upstreams);
});
it('stores and retrieves matcher_value for TLS SNI', async () => {
const matcherValue = ['db.example.com', 'db2.example.com'];
const host = await insertL4Host({
matcherType: 'tls_sni',
matcherValue: JSON.stringify(matcherValue),
});
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.matcherType).toBe('tls_sni');
expect(JSON.parse(row!.matcherValue!)).toEqual(matcherValue);
});
it('stores and retrieves matcher_value for HTTP host', async () => {
const matcherValue = ['api.example.com'];
const host = await insertL4Host({
matcherType: 'http_host',
matcherValue: JSON.stringify(matcherValue),
});
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.matcherType).toBe('http_host');
expect(JSON.parse(row!.matcherValue!)).toEqual(matcherValue);
});
it('matcher_value is null for none matcher', async () => {
const host = await insertL4Host({ matcherType: 'none', matcherValue: null });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.matcherType).toBe('none');
expect(row!.matcherValue).toBeNull();
});
it('matcher_value is null for proxy_protocol matcher', async () => {
const host = await insertL4Host({ matcherType: 'proxy_protocol', matcherValue: null });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.matcherType).toBe('proxy_protocol');
expect(row!.matcherValue).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Boolean fields
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts boolean fields', () => {
it('tls_termination defaults to false', async () => {
const host = await insertL4Host();
expect(Boolean(host.tlsTermination)).toBe(false);
});
it('tls_termination can be set to true', async () => {
const host = await insertL4Host({ tlsTermination: true });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.tlsTermination)).toBe(true);
});
it('proxy_protocol_receive defaults to false', async () => {
const host = await insertL4Host();
expect(Boolean(host.proxyProtocolReceive)).toBe(false);
});
it('proxy_protocol_receive can be set to true', async () => {
const host = await insertL4Host({ proxyProtocolReceive: true });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.proxyProtocolReceive)).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Proxy protocol version
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts proxy protocol version', () => {
it('proxy_protocol_version defaults to null', async () => {
const host = await insertL4Host();
expect(host.proxyProtocolVersion).toBeNull();
});
it('stores v1 proxy protocol version', async () => {
const host = await insertL4Host({ proxyProtocolVersion: 'v1' });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.proxyProtocolVersion).toBe('v1');
});
it('stores v2 proxy protocol version', async () => {
const host = await insertL4Host({ proxyProtocolVersion: 'v2' });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.proxyProtocolVersion).toBe('v2');
});
});
// ---------------------------------------------------------------------------
// Meta field
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts meta', () => {
it('meta can be null', async () => {
const host = await insertL4Host({ meta: null });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.meta).toBeNull();
});
it('stores and retrieves load balancer config via meta', async () => {
const meta = {
load_balancer: {
enabled: true,
policy: 'round_robin',
try_duration: '5s',
try_interval: '250ms',
retries: 3,
active_health_check: { enabled: true, port: 8081, interval: '10s', timeout: '5s' },
passive_health_check: { enabled: true, fail_duration: '30s', max_fails: 5 },
},
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.load_balancer.enabled).toBe(true);
expect(parsed.load_balancer.policy).toBe('round_robin');
expect(parsed.load_balancer.active_health_check.port).toBe(8081);
expect(parsed.load_balancer.passive_health_check.max_fails).toBe(5);
});
it('stores and retrieves DNS resolver config via meta', async () => {
const meta = {
dns_resolver: {
enabled: true,
resolvers: ['1.1.1.1', '8.8.8.8'],
fallbacks: ['8.8.4.4'],
timeout: '5s',
},
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.dns_resolver.enabled).toBe(true);
expect(parsed.dns_resolver.resolvers).toEqual(['1.1.1.1', '8.8.8.8']);
expect(parsed.dns_resolver.timeout).toBe('5s');
});
it('stores and retrieves upstream DNS resolution config via meta', async () => {
const meta = {
upstream_dns_resolution: { enabled: true, family: 'ipv4' },
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.upstream_dns_resolution.enabled).toBe(true);
expect(parsed.upstream_dns_resolution.family).toBe('ipv4');
});
it('stores all three meta features together', async () => {
const meta = {
load_balancer: { enabled: true, policy: 'ip_hash' },
dns_resolver: { enabled: true, resolvers: ['1.1.1.1'] },
upstream_dns_resolution: { enabled: true, family: 'both' },
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.load_balancer.policy).toBe('ip_hash');
expect(parsed.dns_resolver.resolvers).toEqual(['1.1.1.1']);
expect(parsed.upstream_dns_resolution.family).toBe('both');
});
it('stores and retrieves geo blocking config via meta', async () => {
const meta = {
geoblock: {
enabled: true,
block_countries: ['CN', 'RU', 'KP'],
block_continents: ['AF'],
block_asns: [12345],
block_cidrs: ['192.0.2.0/24'],
block_ips: ['203.0.113.1'],
allow_countries: ['US'],
allow_continents: [],
allow_asns: [],
allow_cidrs: ['10.0.0.0/8'],
allow_ips: [],
},
geoblock_mode: 'override',
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.geoblock.enabled).toBe(true);
expect(parsed.geoblock.block_countries).toEqual(['CN', 'RU', 'KP']);
expect(parsed.geoblock.allow_cidrs).toEqual(['10.0.0.0/8']);
expect(parsed.geoblock_mode).toBe('override');
});
it('stores all four meta features together', async () => {
const meta = {
load_balancer: { enabled: true, policy: 'round_robin' },
dns_resolver: { enabled: true, resolvers: ['1.1.1.1'] },
upstream_dns_resolution: { enabled: true, family: 'ipv4' },
geoblock: { enabled: true, block_countries: ['CN'], block_continents: [], block_asns: [], block_cidrs: [], block_ips: [], allow_countries: [], allow_continents: [], allow_asns: [], allow_cidrs: [], allow_ips: [] },
};
const host = await insertL4Host({ meta: JSON.stringify(meta) });
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.load_balancer.policy).toBe('round_robin');
expect(parsed.geoblock.block_countries).toEqual(['CN']);
});
});
// ---------------------------------------------------------------------------
// Update
// ---------------------------------------------------------------------------
describe('l4-proxy-hosts update', () => {
it('updates listen address', async () => {
const host = await insertL4Host({ listenAddress: ':5432' });
await db.update(l4ProxyHosts).set({ listenAddress: ':3306' }).where(eq(l4ProxyHosts.id, host.id));
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.listenAddress).toBe(':3306');
});
it('updates protocol from tcp to udp', async () => {
const host = await insertL4Host({ protocol: 'tcp' });
await db.update(l4ProxyHosts).set({ protocol: 'udp' }).where(eq(l4ProxyHosts.id, host.id));
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.protocol).toBe('udp');
});
it('toggles enabled state', async () => {
const host = await insertL4Host({ enabled: true });
await db.update(l4ProxyHosts).set({ enabled: false }).where(eq(l4ProxyHosts.id, host.id));
const row = await db.query.l4ProxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.enabled)).toBe(false);
});
});