Files
caddy-proxy-manager/tests/e2e/geoblock.spec.ts
akanealw 99819b70ff
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
added caddy-proxy-manager for testing
2026-04-21 22:49:08 +00:00

200 lines
8.8 KiB
TypeScript
Executable File

import { test, expect } from '@playwright/test';
/** Empty geoblock config used to reset state between tests. */
const EMPTY_GEOBLOCK = {
enabled: false,
block_countries: [], block_continents: [], block_asns: [], block_cidrs: [], block_ips: [],
allow_countries: [], allow_continents: [], allow_asns: [], allow_cidrs: [], allow_ips: [],
trusted_proxies: [], fail_closed: false,
response_status: 403, response_body: 'Forbidden',
response_headers: {}, redirect_url: '',
};
/**
* RFC 5737 TEST-NET ranges — routable nowhere, so they won't block real
* traffic when applied to Caddy during tests (unlike 0.0.0.0/0).
*/
const SAFE_BLOCK_CIDR = '198.51.100.0/24'; // TEST-NET-2
const SAFE_ALLOW_CIDR = '203.0.113.0/24'; // TEST-NET-3
const SAFE_BLOCK_CIDR_2 = '192.0.2.0/24'; // TEST-NET-1
const SAFE_ALLOW_CIDR_2 = '233.252.0.0/24'; // MCAST-TEST-NET
const API_GEOBLOCK = 'http://localhost:3000/api/v1/settings/geoblock';
/**
* Find the visible text input inside a TagInput component by its hidden input name.
*/
function cidrInput(parent: ReturnType<typeof test['info']> extends never ? never : any, name: string) {
return parent.locator(`div:has(> input[name="${name}"])`)
.locator('input[type="text"]');
}
test.describe('Geo Blocking — form persistence', () => {
async function resetGeoblock(page: any) {
await page.request.put(API_GEOBLOCK, { data: EMPTY_GEOBLOCK });
}
test.beforeEach(async ({ page }) => {
await resetGeoblock(page);
await page.goto('/settings');
});
test.afterEach(async ({ page }) => {
await resetGeoblock(page);
});
/**
* Regression: Radix Tabs unmount inactive tab content, so only the
* currently-visible tab's hidden inputs were submitted. Saving while on the
* "Block Rules" tab would wipe all allow rules and vice-versa.
*
* Uses RFC 5737 test ranges to avoid blocking real traffic.
*/
test('saving block rules does not wipe allow rules', async ({ page }) => {
const geoSection = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
const enableSwitch = geoSection.getByRole('switch');
if (!(await enableSwitch.isChecked())) {
await enableSwitch.click();
}
await geoSection.getByRole('tab', { name: /allow rules/i }).click();
const allowInput = cidrInput(geoSection, 'geoblock_allow_cidrs');
await allowInput.fill(SAFE_ALLOW_CIDR);
await allowInput.press('Enter');
await expect(geoSection.locator(`text=${SAFE_ALLOW_CIDR}`)).toBeVisible();
await geoSection.getByRole('tab', { name: /block rules/i }).click();
const blockInput = cidrInput(geoSection, 'geoblock_block_cidrs');
await blockInput.fill(SAFE_BLOCK_CIDR);
await blockInput.press('Enter');
await expect(geoSection.locator(`text=${SAFE_BLOCK_CIDR}`)).toBeVisible();
await geoSection.getByRole('button', { name: /save geoblocking settings/i }).click();
await expect(geoSection.locator('text=/saved|success/i')).toBeVisible({ timeout: 10000 });
await page.reload();
const fresh = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
await fresh.getByRole('tab', { name: /block rules/i }).click();
await expect(fresh.locator(`text=${SAFE_BLOCK_CIDR}`)).toBeVisible({ timeout: 5000 });
await fresh.getByRole('tab', { name: /allow rules/i }).click();
await expect(fresh.locator(`text=${SAFE_ALLOW_CIDR}`)).toBeVisible({ timeout: 5000 });
});
test('saving allow rules does not wipe block rules', async ({ page }) => {
const geoSection = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
const enableSwitch = geoSection.getByRole('switch');
if (!(await enableSwitch.isChecked())) {
await enableSwitch.click();
}
await geoSection.getByRole('tab', { name: /block rules/i }).click();
const blockInput = cidrInput(geoSection, 'geoblock_block_cidrs');
await blockInput.fill(SAFE_BLOCK_CIDR_2);
await blockInput.press('Enter');
await expect(geoSection.locator(`text=${SAFE_BLOCK_CIDR_2}`)).toBeVisible();
await geoSection.getByRole('tab', { name: /allow rules/i }).click();
const allowInput = cidrInput(geoSection, 'geoblock_allow_cidrs');
await allowInput.fill(SAFE_ALLOW_CIDR_2);
await allowInput.press('Enter');
await expect(geoSection.locator(`text=${SAFE_ALLOW_CIDR_2}`)).toBeVisible();
await geoSection.getByRole('button', { name: /save geoblocking settings/i }).click();
await expect(geoSection.locator('text=/saved|success/i')).toBeVisible({ timeout: 10000 });
await page.reload();
const fresh = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
await fresh.getByRole('tab', { name: /block rules/i }).click();
await expect(fresh.locator(`text=${SAFE_BLOCK_CIDR_2}`)).toBeVisible({ timeout: 5000 });
await fresh.getByRole('tab', { name: /allow rules/i }).click();
await expect(fresh.locator(`text=${SAFE_ALLOW_CIDR_2}`)).toBeVisible({ timeout: 5000 });
});
/**
* Regression: Radix Accordion unmounts closed content, so advanced
* settings (redirect URL, trusted proxies, response status/body) were
* wiped when saving with the accordion collapsed.
*/
test('advanced settings survive save when accordion is collapsed', async ({ page }) => {
const geoSection = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
const enableSwitch = geoSection.getByRole('switch');
if (!(await enableSwitch.isChecked())) {
await enableSwitch.click();
}
await geoSection.getByRole('button', { name: /trusted proxies/i }).click();
const redirectInput = geoSection.locator('input[name="geoblock_redirect_url"]');
await expect(redirectInput).toBeVisible();
await redirectInput.fill('https://example.com/blocked');
await geoSection.getByRole('button', { name: /trusted proxies/i }).click();
await geoSection.getByRole('button', { name: /save geoblocking settings/i }).click();
await expect(geoSection.locator('text=/saved|success/i')).toBeVisible({ timeout: 10000 });
await page.reload();
const fresh = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
await fresh.getByRole('button', { name: /trusted proxies/i }).click();
await expect(fresh.locator('input[name="geoblock_redirect_url"]'))
.toHaveValue('https://example.com/blocked', { timeout: 5000 });
});
/**
* Tests the LAN Only (RFC1918) preset — values must survive tab switching.
* This test does NOT save, so no Caddy config is affected.
*/
test('LAN Only preset: values survive tab switching', async ({ page }) => {
const geoSection = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
const enableSwitch = geoSection.getByRole('switch');
if (!(await enableSwitch.isChecked())) {
await enableSwitch.click();
}
await geoSection.getByRole('button', { name: /lan only/i }).click();
await expect(geoSection.locator('text=0.0.0.0/0')).toBeVisible();
await geoSection.getByRole('tab', { name: /allow rules/i }).click();
await expect(geoSection.locator('text=10.0.0.0/8')).toBeVisible();
await expect(geoSection.locator('text=172.16.0.0/12')).toBeVisible();
await expect(geoSection.locator('text=192.168.0.0/16')).toBeVisible();
await geoSection.getByRole('tab', { name: /block rules/i }).click();
await expect(geoSection.locator('text=0.0.0.0/0')).toBeVisible();
await geoSection.getByRole('tab', { name: /allow rules/i }).click();
await expect(geoSection.locator('text=10.0.0.0/8')).toBeVisible();
});
/**
* Tests that the LAN Only preset persists after save.
* Saves via UI form, then immediately reads back via API and resets Caddy
* to minimize the window where 0.0.0.0/0 blocks all traffic.
*/
test('LAN Only preset: values persist after save', async ({ page }) => {
const geoSection = page.locator('form', { has: page.getByRole('button', { name: /save geoblocking settings/i }) });
const enableSwitch = geoSection.getByRole('switch');
if (!(await enableSwitch.isChecked())) {
await enableSwitch.click();
}
await geoSection.getByRole('button', { name: /lan only/i }).click();
await geoSection.getByRole('button', { name: /save geoblocking settings/i }).click();
await expect(geoSection.locator('text=/saved|success/i')).toBeVisible({ timeout: 10000 });
// Read saved values via API, then immediately reset to stop blocking traffic
const res = await page.request.get(API_GEOBLOCK);
await resetGeoblock(page);
const saved = await res.json();
expect(saved.block_cidrs).toContain('0.0.0.0/0');
expect(saved.allow_cidrs).toContain('10.0.0.0/8');
expect(saved.allow_cidrs).toContain('172.16.0.0/12');
expect(saved.allow_cidrs).toContain('192.168.0.0/16');
});
});