Files
caddy-proxy-manager/tests/integration/proxy-hosts-meta.test.ts
2026-03-07 16:53:36 +01:00

242 lines
9.2 KiB
TypeScript

/**
* Integration tests: proxy host JSON field serialization.
*
* Verifies that complex nested meta objects (WAF, geo-block, authentik,
* load balancer) survive a round-trip through the database — stored as JSON,
* retrieved and deserialized correctly.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { createTestDb, type TestDb } from '../helpers/db';
import { proxyHosts } from '@/src/lib/db/schema';
import { eq } from 'drizzle-orm';
let db: TestDb;
beforeEach(() => {
db = createTestDb();
});
function nowIso() {
return new Date().toISOString();
}
async function insertHost(overrides: Partial<typeof proxyHosts.$inferInsert> = {}) {
const now = nowIso();
const [host] = await db.insert(proxyHosts).values({
name: 'Test Host',
domains: JSON.stringify(['test.example.com']),
upstreams: JSON.stringify(['backend:8080']),
certificateId: null,
accessListId: null,
sslForced: 0,
hstsEnabled: 0,
hstsSubdomains: 0,
allowWebsocket: 0,
preserveHostHeader: 0,
skipHttpsHostnameValidation: 0,
meta: null,
enabled: 1,
createdAt: now,
updatedAt: now,
...overrides,
}).returning();
return host;
}
// ---------------------------------------------------------------------------
// domains / upstreams JSON
// ---------------------------------------------------------------------------
describe('proxy-hosts JSON fields', () => {
it('stores and retrieves domains array', async () => {
const domains = ['example.com', 'www.example.com'];
const host = await insertHost({ domains: JSON.stringify(domains) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(JSON.parse(row!.domains)).toEqual(domains);
});
it('stores and retrieves multiple upstreams', async () => {
const upstreams = ['backend1:8080', 'backend2:8080', 'backend3:8080'];
const host = await insertHost({ upstreams: JSON.stringify(upstreams) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(JSON.parse(row!.upstreams)).toEqual(upstreams);
});
it('stores upstream with URL containing commas without splitting', () => {
// URLs with commas in query strings must survive round-trip intact
const upstreams = ['http://backend.local/api?a=1,b=2'];
const stored = JSON.stringify(upstreams);
const retrieved = JSON.parse(stored);
expect(retrieved).toEqual(upstreams);
});
});
// ---------------------------------------------------------------------------
// WAF meta round-trip
// ---------------------------------------------------------------------------
describe('proxy-hosts WAF meta', () => {
it('stores and retrieves WAF config with OWASP CRS enabled', async () => {
const wafMeta = {
waf: {
enabled: true,
mode: 'On',
load_owasp_crs: true,
excluded_rule_ids: [942100, 941110],
waf_mode: 'override',
custom_directives: 'SecRuleEngine On',
},
};
const host = await insertHost({ meta: JSON.stringify(wafMeta) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.waf.enabled).toBe(true);
expect(parsed.waf.load_owasp_crs).toBe(true);
expect(parsed.waf.excluded_rule_ids).toEqual([942100, 941110]);
expect(parsed.waf.waf_mode).toBe('override');
});
it('stores and retrieves disabled WAF config', async () => {
const meta = { waf: { enabled: false, waf_mode: 'merge' } };
const host = await insertHost({ meta: JSON.stringify(meta) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.waf.enabled).toBe(false);
expect(parsed.waf.waf_mode).toBe('merge');
});
});
// ---------------------------------------------------------------------------
// Geo-block meta round-trip
// ---------------------------------------------------------------------------
describe('proxy-hosts geo-block meta', () => {
it('stores and retrieves geo-block block list config', async () => {
const meta = {
geoblock_mode: 'block',
geoblock: {
block_countries: ['RU', 'CN', 'KP'],
allow_countries: [],
block_asns: [12345],
block_ips: ['1.2.3.4'],
block_cidrs: ['5.6.0.0/16'],
response_status_code: 403,
fail_closed: true,
},
};
const host = await insertHost({ meta: JSON.stringify(meta) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.geoblock_mode).toBe('block');
expect(parsed.geoblock.block_countries).toEqual(['RU', 'CN', 'KP']);
expect(parsed.geoblock.block_asns).toEqual([12345]);
expect(parsed.geoblock.response_status_code).toBe(403);
expect(parsed.geoblock.fail_closed).toBe(true);
});
it('stores and retrieves geo-block allow list config', async () => {
const meta = {
geoblock_mode: 'block',
geoblock: {
block_countries: [],
allow_countries: ['FI', 'SE', 'NO'],
response_status_code: 403,
fail_closed: false,
},
};
const host = await insertHost({ meta: JSON.stringify(meta) });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
const parsed = JSON.parse(row!.meta!);
expect(parsed.geoblock.allow_countries).toEqual(['FI', 'SE', 'NO']);
expect(parsed.geoblock.fail_closed).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Load balancer meta round-trip
// ---------------------------------------------------------------------------
describe('proxy-hosts load balancer meta', () => {
it('stores and retrieves load balancer config with active health checks', async () => {
const meta = {
load_balancer: {
enabled: true,
policy: 'round_robin',
active_health_check: {
enabled: true,
uri: '/health',
port: 8081,
interval: '30s',
timeout: '5s',
expected_status: 200,
},
passive_health_check: {
enabled: false,
},
cookie_secret: null,
header_field: null,
},
};
const host = await insertHost({ meta: JSON.stringify(meta) });
const row = await db.query.proxyHosts.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.load_balancer.active_health_check.uri).toBe('/health');
expect(parsed.load_balancer.active_health_check.expected_status).toBe(200);
});
});
// ---------------------------------------------------------------------------
// Boolean fields
// ---------------------------------------------------------------------------
describe('proxy-hosts boolean fields', () => {
it('sslForced is stored and retrieved truthy', async () => {
const host = await insertHost({ sslForced: 1 });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
// Drizzle may return SQLite 0/1 as number or as boolean depending on schema mode
expect(Boolean(row!.sslForced)).toBe(true);
});
it('hstsEnabled and hstsSubdomains round-trip correctly', async () => {
const host = await insertHost({ hstsEnabled: 1, hstsSubdomains: 1 });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.hstsEnabled)).toBe(true);
expect(Boolean(row!.hstsSubdomains)).toBe(true);
});
it('allowWebsocket defaults to falsy when not set', async () => {
const host = await insertHost();
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.allowWebsocket)).toBe(false);
});
it('enabled can be set to disabled (falsy)', async () => {
const host = await insertHost({ enabled: 0 });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(Boolean(row!.enabled)).toBe(false);
});
});
// ---------------------------------------------------------------------------
// Null meta field
// ---------------------------------------------------------------------------
describe('proxy-hosts null meta', () => {
it('meta can be null for simple hosts', async () => {
const host = await insertHost({ meta: null });
const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) });
expect(row!.meta).toBeNull();
});
it('multiple hosts can coexist with different meta states', async () => {
const h1 = await insertHost({ name: 'Simple', meta: null });
const h2 = await insertHost({ name: 'With WAF', meta: JSON.stringify({ waf: { enabled: true, waf_mode: 'override' } }) });
const r1 = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, h1.id) });
const r2 = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, h2.id) });
expect(r1!.meta).toBeNull();
expect(JSON.parse(r2!.meta!).waf.enabled).toBe(true);
});
});