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
336 lines
14 KiB
TypeScript
Executable File
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);
|
|
});
|
|
});
|