Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
393 changes: 393 additions & 0 deletions src/squid/config-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,393 @@
/**
* Tests for src/squid/config-generator.ts – generateSquidConfig.
*
* Exercises the public surface of generateSquidConfig:
* - Basic config generation with required fields
* - Domain ACL rules (plain domains, wildcards, HTTP-only, HTTPS-only)
* - Blocked domain handling
* - SSL Bump configuration
* - DLP (Data Loss Prevention) configuration
* - API proxy IP validation and allow rules
* - DNS nameservers configuration
* - Upstream proxy chaining
* - Topology peers configuration
* - Static boilerplate assertions (logging, timeouts, security rules)
* - Error path: invalid apiProxyIp
*/
import { generateSquidConfig } from './config-generator';
import type { SquidConfig } from '../types';

function makeConfig(overrides: Partial<SquidConfig> = {}): SquidConfig {
return {
domains: ['github.com'],
port: 3128,
...overrides,
};
}

describe('generateSquidConfig', () => {
// ── Required boilerplate ────────────────────────────────────────────────────

describe('static boilerplate', () => {
it('emits the awf header comment', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('# Generated by awf');
});

it('disables the pinger', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('pinger_enable off');
});

it('sets the pid filename', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('pid_filename /var/run/squid/squid.pid');
});

it('contains the firewall_detailed logformat', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('logformat firewall_detailed');
});

it('contains the audit_jsonl logformat', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('logformat audit_jsonl');
});

it('contains access_log directives', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('access_log /var/log/squid/access.log firewall_detailed');
expect(config).toContain('access_log /var/log/squid/audit.jsonl audit_jsonl');
});

it('denies raw IPv4 connections', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('acl dst_ipv4');
expect(config).toContain('http_access deny dst_ipv4');
});

it('denies raw IPv6 connections', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('acl dst_ipv6');
expect(config).toContain('http_access deny dst_ipv6');
});

it('deletes forwarded-for headers', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('forwarded_for delete');
});

it('disables via header', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('via off');
});

it('sets read_timeout to 30 minutes', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('read_timeout 30 minutes');
});

it('enables half_closed_clients for SSE streaming', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('half_closed_clients on');
});

it('sets shutdown_lifetime 0 seconds for ephemeral proxy', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('shutdown_lifetime 0 seconds');
});

it('includes localnet ACL definitions', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('acl localnet src 10.0.0.0/8');
expect(config).toContain('acl localnet src 172.16.0.0/12');
expect(config).toContain('acl localnet src 192.168.0.0/16');
});

it('allows localnet after domain filtering', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('http_access allow localnet');
});

it('denies all other traffic at end', () => {
const config = generateSquidConfig(makeConfig());
expect(config).toContain('http_access deny all');
});
});

// ── Port configuration ──────────────────────────────────────────────────────

describe('port configuration', () => {
it('uses the configured port', () => {
const config = generateSquidConfig(makeConfig({ port: 4128 }));
expect(config).toContain('http_port 4128');
});

it('uses default port 3128', () => {
const config = generateSquidConfig(makeConfig({ port: 3128 }));
expect(config).toContain('http_port 3128');
});
});

// ── Domain ACL rules ────────────────────────────────────────────────────────

describe('domain ACL rules', () => {
it('generates allowed_domains ACL for plain domain', () => {
const config = generateSquidConfig(makeConfig({ domains: ['github.com'] }));
expect(config).toContain('acl allowed_domains dstdomain .github.com');
});

it('generates allowed_domains_regex ACL for wildcard domain', () => {
const config = generateSquidConfig(makeConfig({ domains: ['*.example.com'] }));
expect(config).toContain('acl allowed_domains_regex dstdom_regex');
expect(config).toContain('example\\.com');
});

it('generates deny rule for allowed domains', () => {
const config = generateSquidConfig(makeConfig({ domains: ['github.com'] }));
expect(config).toContain('http_access deny !allowed_domains');
});

it('falls through to deny all when no domains configured', () => {
const config = generateSquidConfig(makeConfig({ domains: [] }));
expect(config).toContain('# No domains configured');
expect(config).toContain('http_access deny all');
});

it('generates allowed_http_only ACL for http:// prefixed domain', () => {
const config = generateSquidConfig(makeConfig({ domains: ['http://api.example.com'] }));
expect(config).toContain('acl allowed_http_only dstdomain .api.example.com');
});

it('generates allowed_https_only ACL for https:// prefixed domain', () => {
const config = generateSquidConfig(makeConfig({ domains: ['https://secure.example.com'] }));
expect(config).toContain('acl allowed_https_only dstdomain .secure.example.com');
});

it('handles multiple plain domains', () => {
const config = generateSquidConfig(makeConfig({ domains: ['github.com', 'npmjs.com'] }));
expect(config).toContain('acl allowed_domains dstdomain .github.com');
expect(config).toContain('acl allowed_domains dstdomain .npmjs.com');
});
});

// ── Blocked domains ─────────────────────────────────────────────────────────

describe('blocked domains', () => {
it('generates blocked_domains ACL when blockedDomains provided', () => {
const config = generateSquidConfig(makeConfig({
domains: ['example.com'],
blockedDomains: ['internal.example.com'],
}));
expect(config).toContain('acl blocked_domains dstdomain .internal.example.com');
expect(config).toContain('http_access deny blocked_domains');
});

it('strips protocol prefix from blocked domains', () => {
const config = generateSquidConfig(makeConfig({
domains: ['example.com'],
blockedDomains: ['https://internal.example.com'],
}));
expect(config).toContain('acl blocked_domains dstdomain .internal.example.com');
});

it('generates blocked_domains_regex ACL for wildcard blocked domains', () => {
const config = generateSquidConfig(makeConfig({
domains: ['example.com'],
blockedDomains: ['*.internal.example.com'],
}));
expect(config).toContain('acl blocked_domains_regex dstdom_regex');
expect(config).toContain('http_access deny blocked_domains_regex');
});

it('emits no blocked config when blockedDomains is empty', () => {
const config = generateSquidConfig(makeConfig({ blockedDomains: [] }));
expect(config).not.toContain('blocked_domains');
});

it('emits no blocked config when blockedDomains is undefined', () => {
const config = generateSquidConfig(makeConfig({ blockedDomains: undefined }));
expect(config).not.toContain('blocked_domains');
});
});

// ── SSL Bump ────────────────────────────────────────────────────────────────

describe('SSL Bump', () => {
it('does not include SSL Bump comment when sslBump is false', () => {
const config = generateSquidConfig(makeConfig({ sslBump: false }));
expect(config).not.toContain('SSL Bump mode enabled');
});

it('includes SSL Bump comment when sslBump is true', () => {
const config = generateSquidConfig(makeConfig({
sslBump: true,
caFiles: { certPath: '/certs/ca.pem', keyPath: '/certs/ca.key' },
sslDbPath: '/tmp/ssl_db',
}));
expect(config).toContain('SSL Bump mode enabled');
});

it('generates ssl_bump directives when sslBump with caFiles and sslDbPath', () => {
const config = generateSquidConfig(makeConfig({
sslBump: true,
caFiles: { certPath: '/certs/ca.pem', keyPath: '/certs/ca.key' },
sslDbPath: '/tmp/ssl_db',
}));
expect(config).toContain('ssl_bump');
expect(config).toContain('/certs/ca.pem');
expect(config).toContain('/tmp/ssl_db');
});

it('does not generate ssl_bump config when sslBump is true but caFiles is missing', () => {
const config = generateSquidConfig(makeConfig({ sslBump: true }));
expect(config).not.toContain('ssl_bump stare');
});
});

// ── DLP ─────────────────────────────────────────────────────────────────────

describe('DLP (Data Loss Prevention)', () => {
it('generates DLP ACL when enableDlp is true', () => {
const config = generateSquidConfig(makeConfig({ enableDlp: true }));
expect(config).toContain('dlp');
});

it('does not generate DLP config when enableDlp is false', () => {
const config = generateSquidConfig(makeConfig({ enableDlp: false }));
// No DLP ACL or access rule should appear
expect(config).not.toContain('acl dlp');
});

it('does not generate DLP config when enableDlp is undefined', () => {
const config = generateSquidConfig(makeConfig({ enableDlp: undefined }));
expect(config).not.toContain('acl dlp');
});
});

// ── API proxy IP ────────────────────────────────────────────────────────────

describe('apiProxyIp', () => {
it('adds an allow rule for apiProxyIp before the raw-IP block', () => {
const config = generateSquidConfig(makeConfig({ apiProxyIp: '172.30.0.30' }));
// The apiProxySection should allow traffic to the proxy IP
expect(config).toContain('172.30.0.30');
// The allow must come before the deny dst_ipv4 rule
const allowIdx = config.indexOf('172.30.0.30');
const denyIdx = config.indexOf('http_access deny dst_ipv4');
expect(allowIdx).toBeLessThan(denyIdx);
});

it('throws when apiProxyIp is not a valid IPv4 address', () => {
expect(() => generateSquidConfig(makeConfig({ apiProxyIp: 'not-an-ip' }))).toThrow(/SECURITY.*apiProxyIp/);
});

it('throws when apiProxyIp has invalid octet', () => {
expect(() => generateSquidConfig(makeConfig({ apiProxyIp: '999.999.999.999' }))).toThrow(/SECURITY.*apiProxyIp/);
});

it('does not add apiProxy section when apiProxyIp is undefined', () => {
const config = generateSquidConfig(makeConfig({ apiProxyIp: undefined }));
// Should still have dst_ipv4 deny but no specific 172.30.0.30 allow
expect(config).not.toContain('172.30.0.30');
});

it('includes apiProxyPorts in Safe_ports when provided', () => {
const config = generateSquidConfig(makeConfig({
apiProxyIp: '172.30.0.30',
apiProxyPorts: [10001, 10002],
}));
expect(config).toContain('10001');
expect(config).toContain('10002');
});
});

// ── DNS configuration ────────────────────────────────────────────────────────

describe('DNS configuration', () => {
it('includes dns_nameservers when dnsServers is provided', () => {
const config = generateSquidConfig(makeConfig({ dnsServers: ['8.8.8.8', '1.1.1.1'] }));
expect(config).toContain('dns_nameservers');
expect(config).toContain('8.8.8.8');
expect(config).toContain('1.1.1.1');
});

it('handles empty dnsServers gracefully', () => {
// Should not throw, may use defaults
expect(() => generateSquidConfig(makeConfig({ dnsServers: [] }))).not.toThrow();
});
});

// ── Upstream proxy ───────────────────────────────────────────────────────────

describe('upstream proxy', () => {
it('generates cache_peer directive when upstreamProxy is provided', () => {
const config = generateSquidConfig(makeConfig({
upstreamProxy: { host: 'corporate-proxy.example.com', port: 8080 },
}));
expect(config).toContain('cache_peer corporate-proxy.example.com');
expect(config).toContain('8080');
});

it('does not include cache_peer when upstreamProxy is undefined', () => {
const config = generateSquidConfig(makeConfig({ upstreamProxy: undefined }));
expect(config).not.toContain('cache_peer');
});
});

// ── Topology peers ───────────────────────────────────────────────────────────

describe('topology peers', () => {
it('generates allow rules for topology peers', () => {
const config = generateSquidConfig(makeConfig({
topologyPeers: ['awmg-mcpg', 'awf-cli-proxy'],
}));
expect(config).toContain('awmg-mcpg');
expect(config).toContain('awf-cli-proxy');
});

it('does not generate topology section when topologyPeers is empty', () => {
const config = generateSquidConfig(makeConfig({ topologyPeers: [] }));
// No topology peer ACL should appear
expect(config).not.toContain('awmg-mcpg');
});

it('does not generate topology section when topologyPeers is undefined', () => {
expect(() => generateSquidConfig(makeConfig({ topologyPeers: undefined }))).not.toThrow();
});
});

// ── Host access ──────────────────────────────────────────────────────────────

describe('host access', () => {
it('generates host access config when enableHostAccess is true', () => {
const config = generateSquidConfig(makeConfig({ enableHostAccess: true }));
// Host access enables additional Safe_ports entries
expect(config).toContain('Safe_ports');
});

it('generates custom port ACLs when allowHostPorts is provided', () => {
const config = generateSquidConfig(makeConfig({
enableHostAccess: true,
allowHostPorts: '3000,8080',
}));
expect(config).toContain('3000');
expect(config).toContain('8080');
});
});

// ── Return type ──────────────────────────────────────────────────────────────

describe('return type', () => {
it('returns a non-empty string', () => {
const config = generateSquidConfig(makeConfig());
expect(typeof config).toBe('string');
expect(config.length).toBeGreaterThan(0);
});

it('is a valid string (no null bytes)', () => {
const config = generateSquidConfig(makeConfig());
expect(config).not.toContain('\0');
});
});
});