Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
185 lines
5.3 KiB
TypeScript
Executable File
185 lines
5.3 KiB
TypeScript
Executable File
/**
|
|
* 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' }]);
|
|
});
|
|
});
|