Files
caddy-proxy-manager/tests/e2e/functional/redirects.spec.ts
T
fuomag9 4b5323a7bf feat: add structured redirects and path prefix rewrite for proxy hosts
Adds two new UI-configurable Caddy patterns that previously required raw JSON:
- Per-path redirect rules (from/to/status) emitted as a subroute handler before
  auth so .well-known paths work without login; supports full URLs, cross-domain
  targets, and wildcard path patterns (e.g. /.well-known/*)
- Path prefix rewrite that prepends a segment to every request before proxying
  (e.g. /recipes → upstream sees /recipes/original/path)

Config is stored in the existing meta JSON column (no schema migration). Includes
integration tests for meta serialization and E2E functional tests against a real
Caddy instance covering relative/absolute destinations, all 3xx status codes, and
various wildcard combinations. Adds traefik/whoami to the test stack to verify
rewritten paths actually reach the upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 17:53:33 +01:00

78 lines
3.2 KiB
TypeScript

/**
* Functional tests: per-path redirect rules.
*
* Creates a proxy host with structured redirect rules and verifies that
* Caddy issues the correct redirect responses for matched paths while
* still proxying unmatched paths to the upstream.
*
* The redirects_json hidden field is injected directly (same pattern used
* for other non-labeled form controls like ssl_forced_present) so the test
* doesn't have to click through the MUI Select for each status code.
*
* Domain: func-redirects.test
*/
import { test, expect } from '@playwright/test';
import { httpGet, injectFormFields, waitForRoute } from '../../helpers/http';
const DOMAIN = 'func-redirects.test';
test.describe.serial('Per-path Redirect Rules', () => {
test('setup: create proxy host with redirect rules', async ({ page }) => {
await page.goto('/proxy-hosts');
await page.getByRole('button', { name: /create host/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.getByLabel('Name').fill('Functional Redirects Test');
await page.getByLabel(/domains/i).fill(DOMAIN);
await page.getByPlaceholder('10.0.0.5:8080').first().fill('echo-server:8080');
// Inject redirect rules and form flags directly.
// redirects_json is a hidden input rendered by RedirectsFields whose value
// reflects React state; setting .value just before submit works because no
// React render cycle fires between the injection and form data collection.
await injectFormFields(page, {
ssl_forced_present: 'on',
redirects_json: JSON.stringify([
{ from: '/.well-known/carddav', to: '/remote.php/dav/', status: 301 },
{ from: '/.well-known/caldav', to: '/remote.php/dav/', status: 302 },
]),
});
await page.getByRole('button', { name: /^create$/i }).click();
await expect(page.getByRole('dialog')).not.toBeVisible({ timeout: 15_000 });
await expect(page.getByText('Functional Redirects Test')).toBeVisible({ timeout: 10_000 });
await waitForRoute(DOMAIN);
});
test('matched path receives the configured 301 redirect', async () => {
const res = await httpGet(DOMAIN, '/.well-known/carddav');
expect(res.status).toBe(301);
});
test('301 redirect Location header points to the configured destination', async () => {
const res = await httpGet(DOMAIN, '/.well-known/carddav');
const location = res.headers['location'];
const locationStr = Array.isArray(location) ? location[0] : (location ?? '');
expect(locationStr).toBe('/remote.php/dav/');
});
test('second matched path receives the configured 302 redirect', async () => {
const res = await httpGet(DOMAIN, '/.well-known/caldav');
expect(res.status).toBe(302);
});
test('302 redirect Location header points to the configured destination', async () => {
const res = await httpGet(DOMAIN, '/.well-known/caldav');
const location = res.headers['location'];
const locationStr = Array.isArray(location) ? location[0] : (location ?? '');
expect(locationStr).toBe('/remote.php/dav/');
});
test('unmatched path is proxied normally to the upstream', async () => {
const res = await httpGet(DOMAIN, '/some/other/path');
expect(res.status).toBe(200);
expect(res.body).toContain('echo-ok');
});
});