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 insertProxyHost(overrides: Partial = {}) { const now = nowIso(); const [host] = await db.insert(proxyHosts).values({ name: 'Test Host', domains: JSON.stringify(['example.com']), upstreams: JSON.stringify(['localhost:8080']), sslForced: true, hstsEnabled: true, hstsSubdomains: false, allowWebsocket: true, preserveHostHeader: true, skipHttpsHostnameValidation: false, enabled: true, createdAt: now, updatedAt: now, ...overrides, }).returning(); return host; } describe('proxy-hosts integration', () => { it('inserts proxy host with domains array — retrieved correctly via JSON parse', async () => { const domains = ['example.com', 'www.example.com']; const host = await insertProxyHost({ domains: JSON.stringify(domains), name: 'Multi Domain' }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); expect(JSON.parse(row!.domains)).toEqual(domains); }); it('inserts proxy host with upstreams array — retrieved correctly', async () => { const upstreams = ['app1:8080', 'app2:8080']; const host = await insertProxyHost({ upstreams: JSON.stringify(upstreams), name: 'Load Balanced' }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); expect(JSON.parse(row!.upstreams)).toEqual(upstreams); }); it('enabled field defaults to true', async () => { const host = await insertProxyHost(); expect(host.enabled).toBe(true); }); it('insert and query all returns at least one result', async () => { await insertProxyHost(); const rows = await db.select().from(proxyHosts); expect(rows.length).toBeGreaterThanOrEqual(1); }); it('delete by id removes the host', async () => { const host = await insertProxyHost(); await db.delete(proxyHosts).where(eq(proxyHosts.id, host.id)); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); expect(row).toBeUndefined(); }); it('multiple proxy hosts — count is correct', async () => { await insertProxyHost({ name: 'Host 1', domains: JSON.stringify(['a.com']) }); await insertProxyHost({ name: 'Host 2', domains: JSON.stringify(['b.com']) }); await insertProxyHost({ name: 'Host 3', domains: JSON.stringify(['c.com']) }); const rows = await db.select().from(proxyHosts); expect(rows.length).toBe(3); }); it('hsts and websocket booleans are stored and retrieved correctly', async () => { const host = await insertProxyHost({ hstsEnabled: false, allowWebsocket: false }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); expect(row!.hstsEnabled).toBe(false); expect(row!.allowWebsocket).toBe(false); }); it('stores and retrieves redirect rules via meta JSON', async () => { const redirects = [ { from: '/.well-known/carddav', to: '/remote.php/dav/', status: 301 }, { from: '/.well-known/caldav', to: '/remote.php/dav/', status: 301 }, ]; const host = await insertProxyHost({ name: 'nextcloud', domains: JSON.stringify(['nextcloud.example.com']), upstreams: JSON.stringify(['192.168.1.154:11000']), meta: JSON.stringify({ redirects }), }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); const meta = JSON.parse(row!.meta ?? '{}'); expect(meta.redirects).toHaveLength(2); expect(meta.redirects[0]).toMatchObject({ from: '/.well-known/carddav', to: '/remote.php/dav/', status: 301, }); }); it('stores and retrieves path prefix rewrite via meta JSON', async () => { const host = await insertProxyHost({ name: 'recipes', domains: JSON.stringify(['recipes.example.com']), upstreams: JSON.stringify(['192.168.1.150:8080']), meta: JSON.stringify({ rewrite: { path_prefix: '/recipes' } }), }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); const meta = JSON.parse(row!.meta ?? '{}'); expect(meta.rewrite?.path_prefix).toBe('/recipes'); }); it('stores and retrieves location rules via meta JSON', async () => { const locationRules = [ { path: '/ws/*', upstreams: ['ws-backend:8080', 'ws-backend2:8080'] }, { path: '/api/*', upstreams: ['api:3000'] }, ]; const host = await insertProxyHost({ name: 'multi-backend', domains: JSON.stringify(['app.example.com']), upstreams: JSON.stringify(['frontend:80']), meta: JSON.stringify({ location_rules: locationRules }), }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); const meta = JSON.parse(row!.meta ?? '{}'); expect(meta.location_rules).toHaveLength(2); expect(meta.location_rules[0]).toMatchObject({ path: '/ws/*', upstreams: ['ws-backend:8080', 'ws-backend2:8080'] }); expect(meta.location_rules[1]).toMatchObject({ path: '/api/*', upstreams: ['api:3000'] }); }); it('stores location rules with mixed valid and invalid entries in raw meta', async () => { const locationRules = [ { path: '/good/*', upstreams: ['backend:8080'] }, { path: '', upstreams: ['backend:8080'] }, { path: '/no-upstreams/*', upstreams: [] }, ]; const host = await insertProxyHost({ name: 'mixed-rules', domains: JSON.stringify(['mixed.example.com']), upstreams: JSON.stringify(['backend:80']), meta: JSON.stringify({ location_rules: locationRules }), }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); const meta = JSON.parse(row!.meta ?? '{}'); // Raw DB stores all entries as-is; sanitization happens at model layer (parseMeta) expect(meta.location_rules).toHaveLength(3); expect(meta.location_rules[0].path).toBe('/good/*'); }); it('filters out invalid redirect rules on parse', async () => { const redirects = [ { from: '', to: '/valid', status: 301 }, // missing from — invalid { from: '/valid', to: '', status: 301 }, // missing to — invalid { from: '/ok', to: '/dest', status: 999 }, // bad status — invalid { from: '/good', to: '/dest', status: 302 }, // valid ]; const host = await insertProxyHost({ name: 'test-filter', meta: JSON.stringify({ redirects }), }); const row = await db.query.proxyHosts.findFirst({ where: (t, { eq }) => eq(t.id, host.id) }); // Simulate parseMeta sanitization: only valid rules have non-empty from/to and valid status const meta = JSON.parse(row!.meta ?? '{}'); const valid = (meta.redirects as typeof redirects).filter( (r) => r.from.trim() && r.to.trim() && [301, 302, 307, 308].includes(r.status) ); expect(valid).toHaveLength(1); expect(valid[0].from).toBe('/good'); }); });