- Fix dialog scrollability (flex layout + max-h-[90dvh]) and increase L4 dialog to lg width - Add styled enable card to L4 dialog matching proxy host pattern - Unify section colors across proxy host and L4 dialogs (cyan=LB, emerald=DNS, violet=upstream DNS, rose=geo, amber=mTLS) - Improve light mode contrast: muted-foreground oklch 0.552→0.502, remove opacity modifiers on secondary text - Improve dark mode: boost muted-foreground to 0.85, increase border opacity 10%→16%, input 15%→20% - Add bg-card to DataTable wrapper and bg-muted/40 to table headers for surface hierarchy - Add semantic badge variants (success, warning, info, muted) and StatusChip dark mode fix - Add server-side sortable columns to Proxy Hosts and L4 Proxy Hosts (name, upstream, status, protocol, listen) - Add sortKey to DataTable Column type with clickable sort headers (ArrowUp/Down indicators, URL param driven) - Fix E2E test selectors for shadcn UI (label associations, combobox roles, dropdown menus, mobile drawer) - Add htmlFor/id to proxy host form fields and aria-labels to select triggers for accessibility - Add sorting E2E tests for both proxy host pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
78 lines
3.2 KiB
TypeScript
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.getByRole('table').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');
|
|
});
|
|
});
|