242 lines
9.2 KiB
TypeScript
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);
|
|
});
|
|
});
|