Files
caddy-proxy-manager/tests/unit/caddy-location-rules.test.ts
fuomag9 e26d7a2c3f feat: improve LocationRulesFields UI and add unit tests for buildLocationReverseProxy
- Replace textarea with per-upstream rows (protocol dropdown + address input),
  matching the existing UpstreamInput component pattern
- Export buildLocationReverseProxy for testing
- Add 14 unit tests covering: dial formatting, HTTPS/TLS transport,
  host header preservation, path sanitization, IPv6, mixed upstreams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 14:51:08 +01:00

185 lines
5.3 KiB
TypeScript

/**
* Unit tests for buildLocationReverseProxy (src/lib/caddy.ts).
* Tests the Caddy config building block for location-based routing.
*/
import { describe, it, expect, vi } from 'vitest';
// Undo the global mock so we can import the real function
vi.unmock('@/src/lib/caddy');
import { buildLocationReverseProxy } from '@/src/lib/caddy';
describe('buildLocationReverseProxy', () => {
it('builds basic HTTP reverse proxy with single upstream', () => {
const { safePath, reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/api/*', upstreams: ['backend:3000'] },
false,
false
);
expect(safePath).toBe('/api/*');
expect(reverseProxyHandler).toEqual({
handler: 'reverse_proxy',
upstreams: [{ dial: 'backend:3000' }],
});
});
it('builds reverse proxy with multiple upstreams', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/ws/*', upstreams: ['ws1:8080', 'ws2:8080', 'ws3:8080'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([
{ dial: 'ws1:8080' },
{ dial: 'ws2:8080' },
{ dial: 'ws3:8080' },
]);
});
it('parses http:// upstream URLs into dial format', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/api/*', upstreams: ['http://backend:3000'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([{ dial: 'backend:3000' }]);
expect(reverseProxyHandler.transport).toBeUndefined();
});
it('parses https:// upstream URLs and adds TLS transport', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/secure/*', upstreams: ['https://backend:443'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([{ dial: 'backend:443' }]);
expect(reverseProxyHandler.transport).toEqual({
protocol: 'http',
tls: {},
});
});
it('sets insecure_skip_verify when skipHttpsValidation is true', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/secure/*', upstreams: ['https://backend:443'] },
true,
false
);
expect(reverseProxyHandler.transport).toEqual({
protocol: 'http',
tls: { insecure_skip_verify: true },
});
});
it('does not add TLS transport for HTTP-only upstreams even with skipHttpsValidation', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/api/*', upstreams: ['backend:3000'] },
true,
false
);
expect(reverseProxyHandler.transport).toBeUndefined();
});
it('preserves host header when preserveHostHeader is true', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/api/*', upstreams: ['backend:3000'] },
false,
true
);
expect(reverseProxyHandler.headers).toEqual({
request: { set: { Host: ['{http.request.host}'] } },
});
});
it('does not set host header when preserveHostHeader is false', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/api/*', upstreams: ['backend:3000'] },
false,
false
);
expect(reverseProxyHandler.headers).toBeUndefined();
});
it('sanitizes Caddy placeholder injection from path', () => {
const { safePath } = buildLocationReverseProxy(
{ path: '/api/{http.request.uri}/*', upstreams: ['backend:3000'] },
false,
false
);
expect(safePath).toBe('/api//*');
});
it('returns empty safePath when path is entirely a placeholder', () => {
const { safePath } = buildLocationReverseProxy(
{ path: '{http.request.uri}', upstreams: ['backend:3000'] },
false,
false
);
expect(safePath).toBe('');
});
it('handles mixed HTTP and HTTPS upstreams — TLS transport added', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/mixed/*', upstreams: ['http://backend1:80', 'https://backend2:443'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([
{ dial: 'backend1:80' },
{ dial: 'backend2:443' },
]);
expect(reverseProxyHandler.transport).toEqual({
protocol: 'http',
tls: {},
});
});
it('handles HTTPS upstream with default port 443', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/secure/*', upstreams: ['https://backend'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([{ dial: 'backend:443' }]);
});
it('combines preserve host header + HTTPS transport correctly', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/all-options/*', upstreams: ['https://backend:8443'] },
true,
true
);
expect(reverseProxyHandler.handler).toBe('reverse_proxy');
expect(reverseProxyHandler.headers).toEqual({
request: { set: { Host: ['{http.request.host}'] } },
});
expect(reverseProxyHandler.transport).toEqual({
protocol: 'http',
tls: { insecure_skip_verify: true },
});
});
it('handles IPv6 upstream addresses', () => {
const { reverseProxyHandler } = buildLocationReverseProxy(
{ path: '/v6/*', upstreams: ['[::1]:8080'] },
false,
false
);
expect(reverseProxyHandler.upstreams).toEqual([{ dial: '[::1]:8080' }]);
});
});