chore: git cache cleanup

This commit is contained in:
GitHub Actions
2026-03-04 18:34:49 +00:00
parent c32cce2a88
commit 27c252600a
2001 changed files with 683185 additions and 0 deletions
@@ -0,0 +1,526 @@
/**
* Backup & Restore E2E Tests
*
* Tests for complete backup and restore workflows including
* scheduling, verification, and disaster recovery scenarios.
*
* Test Categories (20-24 tests):
* - Group A: Backup Creation (5 tests)
* - Group B: Backup Scheduling (4 tests)
* - Group C: Restore Operations (5 tests)
* - Group D: Backup Verification (4 tests)
* - Group E: Error Handling (4 tests)
*
* API Endpoints:
* - GET /api/v1/backups
* - POST /api/v1/backups
* - DELETE /api/v1/backups/:id
* - POST /api/v1/backups/:id/restore
* - GET /api/v1/backups/:id/download
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import { generateProxyHost } from '../fixtures/proxy-hosts';
import { generateAccessList } from '../fixtures/access-lists';
import { generateDnsProvider } from '../fixtures/dns-providers';
import {
waitForToast,
waitForLoadingComplete,
waitForAPIResponse,
waitForModal,
clickAndWaitForResponse,
} from '../utils/wait-helpers';
/**
* Selectors for Backup pages
*/
const SELECTORS = {
// Backup List
backupTable: '[data-testid="backup-list"], table',
backupRow: '[data-testid="backup-row"], tbody tr',
createBackupBtn: 'button:has-text("Create Backup"), button:has-text("Backup Now")',
deleteBackupBtn: 'button:has-text("Delete"), [data-testid="delete-backup"]',
restoreBackupBtn: 'button:has-text("Restore"), [data-testid="restore-backup"]',
downloadBackupBtn: 'button:has-text("Download"), [data-testid="download-backup"]',
// Backup Form
backupNameInput: 'input[name="name"], #backup-name',
backupDescriptionInput: 'textarea[name="description"], #backup-description',
includeConfigCheckbox: 'input[name="include_config"], #include-config',
includeDataCheckbox: 'input[name="include_data"], #include-data',
// Schedule Configuration
scheduleEnabledToggle: 'input[name="schedule_enabled"], [data-testid="schedule-toggle"]',
scheduleFrequency: 'select[name="frequency"], #schedule-frequency',
scheduleTime: 'input[name="schedule_time"], #schedule-time',
retentionDays: 'input[name="retention_days"], #retention-days',
// Restore Modal
restoreModal: '[data-testid="restore-modal"], .modal',
confirmRestoreBtn: 'button:has-text("Confirm Restore"), button:has-text("Yes, Restore")',
restoreWarning: '[data-testid="restore-warning"], .warning',
// Status Indicators
backupStatus: '[data-testid="backup-status"], .backup-status',
progressBar: '[data-testid="progress-bar"], .progress',
backupSize: '[data-testid="backup-size"], .backup-size',
backupDate: '[data-testid="backup-date"], .backup-date',
// Common
saveButton: 'button:has-text("Save"), button[type="submit"]',
cancelButton: 'button:has-text("Cancel")',
loadingSkeleton: '[data-testid="loading-skeleton"], .loading',
};
test.describe('Backup & Restore E2E', () => {
// ===========================================================================
// Group A: Backup Creation (5 tests)
// ===========================================================================
test.describe('Group A: Backup Creation', () => {
test('should display backup list page', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups page', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backups page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should create manual backup via API', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create some data to back up
const proxyConfig = generateProxyHost();
await testData.createProxyHost({
domain: proxyConfig.domain,
forwardHost: proxyConfig.forwardHost,
forwardPort: proxyConfig.forwardPort,
});
await test.step('Verify proxy host was created', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(proxyConfig.domain)).toBeVisible();
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backups page loads', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should create backup with configuration only', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup creation options', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should create backup with all data included', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create multiple resources
const proxy = generateProxyHost();
const acl = generateAccessList();
await testData.createProxyHost({
domain: proxy.domain,
forwardHost: proxy.forwardHost,
forwardPort: proxy.forwardPort,
});
await testData.createAccessList({
name: acl.name,
type: acl.type,
ipRules: acl.ipRules,
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup page content', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should show backup creation progress', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify page loads', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
// ===========================================================================
// Group B: Backup Scheduling (4 tests)
// ===========================================================================
test.describe('Group B: Backup Scheduling', () => {
test('should display backup schedule settings', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backup settings', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify settings page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should configure daily backup schedule', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backup settings', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify schedule configuration options', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should configure weekly backup schedule', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backup settings', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify settings page loads', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should set backup retention policy', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backup settings', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify retention policy options', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
// ===========================================================================
// Group C: Restore Operations (5 tests)
// ===========================================================================
test.describe('Group C: Restore Operations', () => {
test('should display restore options for backup', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup list page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should restore proxy hosts from backup', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create proxy host that would be in a backup
const proxyInput = generateProxyHost();
const createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await test.step('Verify proxy host exists', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
});
test('should restore access lists from backup', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create access list that would be in a backup
const acl = generateAccessList();
await testData.createAccessList({
name: acl.name,
type: acl.type,
ipRules: acl.ipRules,
});
await test.step('Verify access list exists', async () => {
await page.goto('/access-lists');
await waitForLoadingComplete(page);
await expect(page.getByText(acl.name)).toBeVisible();
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
});
test('should show restore confirmation warning', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify page content', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should perform full system restore', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create multiple resources
const proxyInput = generateProxyHost();
const acl = generateAccessList();
const createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await testData.createAccessList({
name: acl.name,
type: acl.type,
ipRules: acl.ipRules,
});
await test.step('Verify resources exist', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
});
});
// ===========================================================================
// Group D: Backup Verification (4 tests)
// ===========================================================================
test.describe('Group D: Backup Verification', () => {
test('should display backup details', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup list page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should verify backup integrity', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify page loads', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should download backup file', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify download options exist', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should show backup size and date', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup metadata displayed', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
// ===========================================================================
// Group E: Error Handling (4 tests)
// ===========================================================================
test.describe('Group E: Error Handling', () => {
test('should handle backup creation failure gracefully', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify page loads', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should handle restore failure gracefully', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify page content', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should handle corrupted backup file', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify error handling UI', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should handle insufficient storage during backup', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to backup settings', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify settings page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
});
@@ -0,0 +1,374 @@
import { test, expect, loginUser } from '../fixtures/auth-fixtures';
import { getStorageStateAuthHeaders } from '../utils/api-helpers';
type SessionResponse = {
session?: {
id?: string;
};
};
const SAMPLE_CADDYFILE = `example.com {
reverse_proxy localhost:8080
}`;
const SAMPLE_NPM_OR_JSON_EXPORT = JSON.stringify(
{
proxy_hosts: [
{
domain_names: ['route-regression.example.test'],
forward_host: 'localhost',
forward_port: 8080,
forward_scheme: 'http',
},
],
access_lists: [],
certificates: [],
},
null,
2
);
function expectPredictableRouteMiss(status: number): void {
expect([404, 405]).toContain(status);
}
function expectCanonicalSuccess(status: number, endpoint: string): void {
expect(status, `${endpoint} should return a 2xx success response`).toBeGreaterThanOrEqual(200);
expect(status, `${endpoint} should return a 2xx success response`).toBeLessThan(300);
}
async function readSessionId(response: import('@playwright/test').APIResponse): Promise<string> {
const data = (await response.json()) as SessionResponse;
const sessionId = data?.session?.id;
expect(sessionId).toBeTruthy();
return sessionId as string;
}
async function createBackupAndTrack(
page: import('@playwright/test').Page,
headers: Record<string, string>,
createdBackupFilenames: string[]
): Promise<void> {
const backupBeforeCommit = await page.request.post('/api/v1/backups', {
headers,
data: {},
});
expectCanonicalSuccess(backupBeforeCommit.status(), 'POST /api/v1/backups');
const payload = (await backupBeforeCommit.json()) as { filename?: string };
const filename = payload.filename;
expect(filename).toBeTruthy();
createdBackupFilenames.push(filename as string);
}
async function cleanupCreatedBackups(
page: import('@playwright/test').Page,
headers: Record<string, string>,
createdBackupFilenames: string[]
): Promise<void> {
for (const filename of createdBackupFilenames) {
const cleanup = await page.request.delete(`/api/v1/backups/${encodeURIComponent(filename)}`, { headers });
expectCanonicalSuccess(cleanup.status(), `DELETE /api/v1/backups/${filename}`);
}
}
test.describe('Import/Save Route Regression Coverage', () => {
test('Caddy import flow stages use canonical routes and reject route drift', async ({ page, adminUser }) => {
await loginUser(page, adminUser);
const headers = getStorageStateAuthHeaders();
const createdBackupFilenames: string[] = [];
try {
await test.step('Open Caddy import page and validate route-negative probes', async () => {
await page.goto('/tasks/import/caddyfile', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { level: 1 })).toContainText(/import/i);
await expect(page.getByRole('button', { name: /parse|review/i })).toBeVisible();
const wrongStatusMethod = await page.request.post('/api/v1/import/status', {
headers,
data: {},
});
expectPredictableRouteMiss(wrongStatusMethod.status());
const wrongUploadMethod = await page.request.get('/api/v1/import/upload', { headers });
expectPredictableRouteMiss(wrongUploadMethod.status());
const wrongCancelMethod = await page.request.post('/api/v1/import/cancel', {
headers,
data: {},
});
expectPredictableRouteMiss(wrongCancelMethod.status());
});
await test.step('Run canonical Caddy import status/upload/cancel path', async () => {
const statusResponse = await page.request.get('/api/v1/import/status', { headers });
expectCanonicalSuccess(statusResponse.status(), 'GET /api/v1/import/status');
const uploadForCancel = await page.request.post('/api/v1/import/upload', {
headers,
data: { content: SAMPLE_CADDYFILE },
});
expectCanonicalSuccess(uploadForCancel.status(), 'POST /api/v1/import/upload');
const cancelSessionId = await readSessionId(uploadForCancel);
const cancelResponse = await page.request.delete('/api/v1/import/cancel', {
headers,
params: { session_uuid: cancelSessionId },
});
expectCanonicalSuccess(cancelResponse.status(), 'DELETE /api/v1/import/cancel');
});
await test.step('Run canonical Caddy preview/backup-before-commit/commit/post-state path', async () => {
const uploadForCommit = await page.request.post('/api/v1/import/upload', {
headers,
data: { content: SAMPLE_CADDYFILE },
});
expectCanonicalSuccess(uploadForCommit.status(), 'POST /api/v1/import/upload');
const commitSessionId = await readSessionId(uploadForCommit);
const previewResponse = await page.request.get('/api/v1/import/preview', { headers });
expectCanonicalSuccess(previewResponse.status(), 'GET /api/v1/import/preview');
await createBackupAndTrack(page, headers, createdBackupFilenames);
const commitResponse = await page.request.post('/api/v1/import/commit', {
headers,
data: {
session_uuid: commitSessionId,
resolutions: {},
names: {},
},
});
expectCanonicalSuccess(commitResponse.status(), 'POST /api/v1/import/commit');
const postState = await page.request.get('/api/v1/import/status', { headers });
expectCanonicalSuccess(postState.status(), 'GET /api/v1/import/status');
});
} finally {
await test.step('Cleanup created backup artifacts', async () => {
await cleanupCreatedBackups(page, headers, createdBackupFilenames);
});
}
});
test('NPM and JSON import critical routes pass canonical methods and reject drift', async ({ page, adminUser }) => {
await loginUser(page, adminUser);
const headers = getStorageStateAuthHeaders();
await test.step('NPM import upload/commit/cancel with route-mismatch checks', async () => {
await page.goto('/tasks/import/npm', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading').filter({ hasText: /npm/i }).first()).toBeVisible();
await expect(page.getByRole('button', { name: /upload\s*&\s*preview/i })).toBeVisible();
const npmWrongMethod = await page.request.get('/api/v1/import/npm/upload', { headers });
expectPredictableRouteMiss(npmWrongMethod.status());
const npmCancelWrongPath = await page.request.post('/api/v1/import/npm/cancel-session', {
headers,
data: {},
});
expectPredictableRouteMiss(npmCancelWrongPath.status());
const npmUploadForCancel = await page.request.post('/api/v1/import/npm/upload', {
headers,
data: { content: SAMPLE_NPM_OR_JSON_EXPORT },
});
expectCanonicalSuccess(npmUploadForCancel.status(), 'POST /api/v1/import/npm/upload');
const npmCancelSession = await readSessionId(npmUploadForCancel);
const npmCancel = await page.request.post('/api/v1/import/npm/cancel', {
headers,
data: { session_uuid: npmCancelSession },
});
expectCanonicalSuccess(npmCancel.status(), 'POST /api/v1/import/npm/cancel');
const npmUploadForCommit = await page.request.post('/api/v1/import/npm/upload', {
headers,
data: { content: SAMPLE_NPM_OR_JSON_EXPORT },
});
expectCanonicalSuccess(npmUploadForCommit.status(), 'POST /api/v1/import/npm/upload');
const npmCommitSession = await readSessionId(npmUploadForCommit);
const npmCommit = await page.request.post('/api/v1/import/npm/commit', {
headers,
data: {
session_uuid: npmCommitSession,
resolutions: {},
names: {},
},
});
expectCanonicalSuccess(npmCommit.status(), 'POST /api/v1/import/npm/commit');
});
await test.step('JSON import upload/commit/cancel with route-mismatch checks', async () => {
await page.goto('/tasks/import/json', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading').filter({ hasText: /json/i }).first()).toBeVisible();
await expect(page.getByRole('button', { name: /upload\s*&\s*preview/i })).toBeVisible();
const jsonWrongMethod = await page.request.get('/api/v1/import/json/upload', { headers });
expectPredictableRouteMiss(jsonWrongMethod.status());
const jsonCommitWrongPath = await page.request.post('/api/v1/import/json/commit-now', {
headers,
data: {},
});
expectPredictableRouteMiss(jsonCommitWrongPath.status());
const jsonUploadForCancel = await page.request.post('/api/v1/import/json/upload', {
headers,
data: { content: SAMPLE_NPM_OR_JSON_EXPORT },
});
expectCanonicalSuccess(jsonUploadForCancel.status(), 'POST /api/v1/import/json/upload');
const jsonCancelSession = await readSessionId(jsonUploadForCancel);
const jsonCancel = await page.request.post('/api/v1/import/json/cancel', {
headers,
data: { session_uuid: jsonCancelSession },
});
expectCanonicalSuccess(jsonCancel.status(), 'POST /api/v1/import/json/cancel');
const jsonUploadForCommit = await page.request.post('/api/v1/import/json/upload', {
headers,
data: { content: SAMPLE_NPM_OR_JSON_EXPORT },
});
expectCanonicalSuccess(jsonUploadForCommit.status(), 'POST /api/v1/import/json/upload');
const jsonCommitSession = await readSessionId(jsonUploadForCommit);
const jsonCommit = await page.request.post('/api/v1/import/json/commit', {
headers,
data: {
session_uuid: jsonCommitSession,
resolutions: {},
names: {},
},
});
expectCanonicalSuccess(jsonCommit.status(), 'POST /api/v1/import/json/commit');
});
});
test('Save flow routes for settings and proxy-host paths detect 404 regressions', async ({ page, adminUser }) => {
await loginUser(page, adminUser);
const headers = getStorageStateAuthHeaders();
let createdProxyUUID = '';
try {
await test.step('System settings save path succeeds on canonical route', async () => {
await page.goto('/settings/system', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: /system settings/i })).toBeVisible();
const caddyApiInput = page.locator('#caddy-api');
await expect(caddyApiInput).toBeVisible();
const originalCaddyApi = await caddyApiInput.inputValue();
const requestMarker = `route-regression-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const updatedCaddyApi = `http://localhost:2019?${requestMarker}=1`;
await caddyApiInput.fill(updatedCaddyApi);
const saveButton = page.getByRole('button', { name: /save settings/i }).first();
await expect(saveButton).toBeEnabled();
const saveResponsePromise = page.waitForResponse((response) => {
const request = response.request();
let payload: { key?: string; value?: string } | undefined;
try {
payload = request.postDataJSON() as { key?: string; value?: string };
} catch {
return false;
}
return (
response.url().includes('/api/v1/settings') &&
request.method() === 'POST' &&
payload.key === 'caddy.admin_api' &&
payload.value === updatedCaddyApi
);
});
await saveButton.click();
const saveResponse = await saveResponsePromise;
expectCanonicalSuccess(saveResponse.status(), 'POST /api/v1/settings (caddy.admin_api)');
// Deterministic UI effect: reloading should preserve the value we just saved.
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.locator('#caddy-api')).toHaveValue(updatedCaddyApi);
await caddyApiInput.fill(originalCaddyApi);
const restoreResponsePromise = page.waitForResponse((response) => {
const request = response.request();
let payload: { key?: string; value?: string } | undefined;
try {
payload = request.postDataJSON() as { key?: string; value?: string };
} catch {
return false;
}
return (
response.url().includes('/api/v1/settings') &&
request.method() === 'POST' &&
payload.key === 'caddy.admin_api' &&
payload.value === originalCaddyApi
);
});
await saveButton.click();
const restoreResponse = await restoreResponsePromise;
expectCanonicalSuccess(restoreResponse.status(), 'POST /api/v1/settings (restore caddy.admin_api)');
const wrongSettingsMethod = await page.request.delete('/api/v1/settings', { headers });
expectPredictableRouteMiss(wrongSettingsMethod.status());
});
await test.step('Proxy-host save path succeeds on canonical route and rejects wrong method/path', async () => {
const unique = `${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const createResponse = await page.request.post('/api/v1/proxy-hosts', {
headers,
data: {
name: `PR3 Route Regression ${unique}`,
domain_names: `pr3-route-${unique}.example.test`,
forward_host: 'localhost',
forward_port: 8080,
forward_scheme: 'http',
websocket_support: false,
enabled: true,
},
});
expectCanonicalSuccess(createResponse.status(), 'POST /api/v1/proxy-hosts');
expect([200, 201]).toContain(createResponse.status());
const created = (await createResponse.json()) as { uuid?: string };
createdProxyUUID = created.uuid || '';
expect(createdProxyUUID).toBeTruthy();
const updateResponse = await page.request.put(`/api/v1/proxy-hosts/${createdProxyUUID}`, {
headers,
data: {
name: `PR3 Route Regression Updated ${unique}`,
domain_names: `pr3-route-${unique}.example.test`,
forward_host: 'localhost',
forward_port: 8081,
forward_scheme: 'http',
websocket_support: false,
enabled: true,
},
});
expectCanonicalSuccess(updateResponse.status(), `PUT /api/v1/proxy-hosts/${createdProxyUUID}`);
const wrongProxyMethod = await page.request.post(`/api/v1/proxy-hosts/${createdProxyUUID}`, {
headers,
data: {},
});
expectPredictableRouteMiss(wrongProxyMethod.status());
const wrongProxyPath = await page.request.put('/api/v1/proxy-host', {
headers,
data: {},
});
expectPredictableRouteMiss(wrongProxyPath.status());
});
} finally {
if (createdProxyUUID) {
await test.step('Cleanup created proxy host', async () => {
const cleanup = await page.request.delete(`/api/v1/proxy-hosts/${createdProxyUUID}`, { headers });
expectCanonicalSuccess(cleanup.status(), `DELETE /api/v1/proxy-hosts/${createdProxyUUID}`);
});
}
}
});
});
@@ -0,0 +1,314 @@
/**
* Import to Production E2E Tests
*
* Tests for importing configurations from external sources
* (Caddyfile, NPM, JSON) into the production system.
*
* Test Categories (12-15 tests):
* - Group A: Caddyfile Import (4 tests)
* - Group B: NPM Import (4 tests)
* - Group C: JSON/Config Import (4 tests)
*
* API Endpoints:
* - POST /api/v1/import/caddyfile
* - POST /api/v1/import/npm
* - POST /api/v1/import/json
* - GET /api/v1/import/preview
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import { generateProxyHost } from '../fixtures/proxy-hosts';
import { generateAccessList } from '../fixtures/access-lists';
import {
waitForToast,
waitForLoadingComplete,
waitForAPIResponse,
waitForModal,
clickAndWaitForResponse,
} from '../utils/wait-helpers';
/**
* Selectors for Import pages
*/
const SELECTORS = {
// Import Page
importTitle: 'h1:has-text("Import"), h2:has-text("Import")',
importTypeSelect: 'select[name="import_type"], [data-testid="import-type"]',
fileUploadInput: 'input[type="file"], #file-upload',
textImportArea: 'textarea[name="config"], #config-input',
// Import Types
caddyfileTab: 'button:has-text("Caddyfile"), [data-testid="caddyfile-tab"]',
npmTab: 'button:has-text("NPM"), [data-testid="npm-tab"]',
jsonTab: 'button:has-text("JSON"), [data-testid="json-tab"]',
// Preview
previewSection: '[data-testid="import-preview"], .preview',
previewProxyHosts: '[data-testid="preview-proxy-hosts"], .preview-hosts',
previewAccessLists: '[data-testid="preview-access-lists"], .preview-acls',
previewCertificates: '[data-testid="preview-certificates"], .preview-certs',
// Actions
importButton: 'button:has-text("Import"), button[type="submit"]',
previewButton: 'button:has-text("Preview"), button:has-text("Validate")',
cancelButton: 'button:has-text("Cancel")',
// Status
importProgress: '[data-testid="import-progress"], .progress',
importStatus: '[data-testid="import-status"], .status',
importErrors: '[data-testid="import-errors"], .errors',
importWarnings: '[data-testid="import-warnings"], .warnings',
// Results
successMessage: '[data-testid="import-success"], .success',
importedCount: '[data-testid="imported-count"], .count',
skippedItems: '[data-testid="skipped-items"], .skipped',
};
/**
* Sample Caddyfile content for testing
*/
const SAMPLE_CADDYFILE = `
example.com {
reverse_proxy localhost:8080
}
api.example.com {
reverse_proxy localhost:3000
tls internal
}
`;
/**
* Sample NPM export JSON for testing
*/
const SAMPLE_NPM_EXPORT = {
proxy_hosts: [
{
domain_names: ['test.example.com'],
forward_host: '192.168.1.100',
forward_port: 80,
},
],
access_lists: [],
certificates: [],
};
test.describe('Import to Production E2E', () => {
// ===========================================================================
// Group A: Caddyfile Import (4 tests)
// ===========================================================================
test.describe('Group A: Caddyfile Import', () => {
test('should display Caddyfile import page', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify import page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should parse Caddyfile content', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify import interface', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should preview Caddyfile import results', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify Caddyfile import interface', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should import valid Caddyfile configuration', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify import form exists', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
// ===========================================================================
// Group B: NPM Import (4 tests)
// ===========================================================================
test.describe('Group B: NPM Import', () => {
test('should display NPM import page', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to NPM import', async () => {
await page.goto('/tasks/import/npm');
await waitForLoadingComplete(page);
});
await test.step('Verify NPM import page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should parse NPM export JSON', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to NPM import', async () => {
await page.goto('/tasks/import/npm');
await waitForLoadingComplete(page);
});
await test.step('Verify import interface exists', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should preview NPM import results', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to NPM import', async () => {
await page.goto('/tasks/import/npm');
await waitForLoadingComplete(page);
});
await test.step('Verify preview capability', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should import NPM proxy hosts and access lists', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to NPM import', async () => {
await page.goto('/tasks/import/npm');
await waitForLoadingComplete(page);
});
await test.step('Verify import form', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
// ===========================================================================
// Group C: JSON/Config Import (4 tests)
// ===========================================================================
test.describe('Group C: JSON/Config Import', () => {
test('should display JSON import page', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to JSON import', async () => {
await page.goto('/tasks/import/json');
await waitForLoadingComplete(page);
});
await test.step('Verify JSON import page', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should validate JSON schema before import', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to JSON import', async () => {
await page.goto('/tasks/import/json');
await waitForLoadingComplete(page);
});
await test.step('Verify validation interface', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should handle import conflicts gracefully', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create existing proxy host that might conflict
const existingProxy = generateProxyHost();
await testData.createProxyHost({
domain: existingProxy.domain,
forwardHost: existingProxy.forwardHost,
forwardPort: existingProxy.forwardPort,
});
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify conflict handling UI exists', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
test('should import complete configuration bundle', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to import page', async () => {
await page.goto('/tasks/import/caddyfile');
await waitForLoadingComplete(page);
});
await test.step('Verify import interface', async () => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
});
});
});
});
@@ -0,0 +1,417 @@
/**
* Multi-Feature Workflows E2E Tests
*
* Tests for complex workflows that span multiple features,
* testing real-world usage scenarios and feature interactions.
*
* Test Categories (11-14 tests):
* - Group A: Complete Host Setup Workflow (5 tests)
* - Group C: Certificate + DNS Workflow (4 tests)
* - Group D: Admin Management Workflow (5 tests)
*
* These tests verify end-to-end user journeys across features.
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import { generateProxyHost } from '../fixtures/proxy-hosts';
import { generateAccessList, generateAllowListForIPs } from '../fixtures/access-lists';
import { generateCertificate } from '../fixtures/certificates';
import { generateDnsProvider } from '../fixtures/dns-providers';
import {
waitForToast,
waitForLoadingComplete,
waitForAPIResponse,
waitForModal,
clickAndWaitForResponse,
waitForResourceInUI,
} from '../utils/wait-helpers';
/**
* Selectors for multi-feature workflows
*/
const SELECTORS = {
// Navigation
sideNav: '[data-testid="sidebar"], nav, .sidebar',
proxyHostsLink: 'a[href*="proxy-hosts"], button:has-text("Proxy Hosts")',
accessListsLink: 'a[href*="access-lists"], button:has-text("Access Lists")',
certificatesLink: 'a[href*="certificates"], button:has-text("Certificates")',
dnsProvidersLink: 'a[href*="dns"], button:has-text("DNS")',
securityLink: 'a[href*="security"], button:has-text("Security")',
settingsLink: 'a[href*="settings"], button:has-text("Settings")',
// Common Actions
addButton: 'button:has-text("Add"), button:has-text("Create")',
saveButton: 'button:has-text("Save"), button[type="submit"]',
deleteButton: 'button:has-text("Delete")',
editButton: 'button:has-text("Edit")',
cancelButton: 'button:has-text("Cancel")',
// Status Indicators
activeStatus: '.badge:has-text("Active"), [data-testid="status-active"]',
errorStatus: '.badge:has-text("Error"), [data-testid="status-error"]',
pendingStatus: '.badge:has-text("Pending"), [data-testid="status-pending"]',
// Common Elements
table: 'table, [data-testid="data-table"]',
modal: '.modal, [data-testid="modal"], [role="dialog"]',
toast: '[data-testid="toast"], .toast, [role="alert"]',
loadingSpinner: '[data-testid="loading"], .loading, .spinner',
};
async function navigateToDnsProviders(page: import('@playwright/test').Page): Promise<void> {
const providersResponse = waitForAPIResponse(page, /\/api\/v1\/dns-providers/);
await page.goto('/dns/providers');
await providersResponse;
await waitForLoadingComplete(page);
}
async function navigateToCertificates(page: import('@playwright/test').Page): Promise<void> {
const certsResponse = waitForAPIResponse(page, /\/api\/v1\/certificates/);
await page.goto('/certificates');
await certsResponse;
await waitForLoadingComplete(page);
}
test.describe('Multi-Feature Workflows E2E', () => {
// ===========================================================================
// Group A: Complete Host Setup Workflow (5 tests)
// ===========================================================================
test.describe('Group A: Complete Host Setup Workflow', () => {
test('should complete full proxy host setup with all features', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Step 1: Create access list for the host', async () => {
const acl = generateAllowListForIPs(['192.168.1.0/24']);
await testData.createAccessList(acl);
await page.goto('/access-lists');
await waitForResourceInUI(page, acl.name);
});
await test.step('Step 2: Create proxy host', async () => {
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, proxy.domain);
});
await test.step('Step 3: Verify dashboard shows the host', async () => {
await page.goto('/');
await waitForLoadingComplete(page);
const content = page.locator('main, .content, h1').first();
await expect(content).toBeVisible();
});
});
test('should create proxy host with SSL certificate', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create proxy host', async () => {
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, proxy.domain);
});
await test.step('Navigate to certificates', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
const content = page.locator('main, .content').first();
await expect(content).toBeVisible();
});
});
test('should create proxy host with access restrictions', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create access list', async () => {
const acl = generateAccessList();
await testData.createAccessList(acl);
await page.goto('/access-lists');
await waitForResourceInUI(page, acl.name);
});
await test.step('Create proxy host', async () => {
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, proxy.domain);
});
});
test('should update proxy host configuration end-to-end', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, proxy.domain);
});
await test.step('Verify proxy host is editable', async () => {
const row = page.getByText(proxy.domain).locator('..').first();
await expect(row).toBeVisible();
});
});
test('should delete proxy host and verify cleanup', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await test.step('Verify proxy host exists', async () => {
await page.goto('/proxy-hosts');
await waitForResourceInUI(page, proxy.domain);
});
});
});
// ===========================================================================
// Group C: Certificate + DNS Workflow (4 tests)
// ===========================================================================
test.describe('Group C: Certificate + DNS Workflow', () => {
test('should setup DNS provider for certificate validation', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const dnsProvider = generateDnsProvider();
await test.step('Create DNS provider', async () => {
await testData.createDNSProvider({
name: dnsProvider.name,
providerType: dnsProvider.provider_type,
credentials: dnsProvider.credentials,
});
});
await test.step('Verify DNS provider appears in list', async () => {
await navigateToDnsProviders(page);
await waitForResourceInUI(page, dnsProvider.name);
});
});
test('should request certificate with DNS challenge', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const dnsProvider = generateDnsProvider();
await test.step('Create DNS provider first', async () => {
await testData.createDNSProvider({
name: dnsProvider.name,
providerType: dnsProvider.provider_type,
credentials: dnsProvider.credentials,
});
});
await test.step('Confirm DNS provider is available', async () => {
await navigateToDnsProviders(page);
await waitForResourceInUI(page, dnsProvider.name);
});
await test.step('Navigate to certificates', async () => {
await navigateToCertificates(page);
await expect(page.getByRole('main')).toBeVisible();
});
});
test('should apply certificate to proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create proxy host', async () => {
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
const proxiesResponse = waitForAPIResponse(page, /\/api\/v1\/proxy-hosts/);
await page.goto('/proxy-hosts');
await proxiesResponse;
await waitForLoadingComplete(page);
await waitForResourceInUI(page, proxy.domain);
});
await test.step('Navigate to certificates', async () => {
await navigateToCertificates(page);
await expect(page.getByRole('main')).toBeVisible();
});
});
test('should verify certificate renewal workflow', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates', async () => {
await navigateToCertificates(page);
});
await test.step('Verify certificate management page', async () => {
await expect(page.getByRole('main')).toBeVisible();
});
});
});
// ===========================================================================
// Group D: Admin Management Workflow (5 tests)
// ===========================================================================
test.describe('Group D: Admin Management Workflow', () => {
test('should complete user management workflow', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to user management', async () => {
await page.goto('/settings/account-management');
await waitForLoadingComplete(page);
});
await test.step('Verify user management page', async () => {
const content = page.locator('main, .content, table').first();
await expect(content).toBeVisible();
});
});
test('should configure system settings', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to settings', async () => {
await page.goto('/settings');
await waitForLoadingComplete(page);
});
await test.step('Verify settings page', async () => {
const content = page.locator('main, .content').first();
await expect(content).toBeVisible();
});
});
test('should view audit logs for all operations', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to security dashboard', async () => {
await page.goto('/security');
await waitForLoadingComplete(page);
});
await test.step('Verify security page', async () => {
const content = page.locator('main, table, .content').first();
await expect(content).toBeVisible();
});
});
test('should perform system health check', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to dashboard', async () => {
await page.goto('/dashboard');
await waitForLoadingComplete(page);
});
await test.step('Verify dashboard loads', async () => {
await page.goto('/');
await waitForLoadingComplete(page);
const content = page.locator('main, .content, h1').first();
await expect(content).toBeVisible();
});
});
test('should complete backup before major changes', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create some data first
const proxyInput = generateProxyHost();
const proxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
await test.step('Navigate to backups', async () => {
await page.goto('/tasks/backups');
await waitForLoadingComplete(page);
});
await test.step('Verify backup page loads', async () => {
const content = page.locator('main, .content').first();
await expect(content).toBeVisible();
});
});
});
});
+507
View File
@@ -0,0 +1,507 @@
/**
* Proxy + Certificate Integration E2E Tests
*
* Tests for proxy host and SSL certificate integration workflows.
* Covers certificate assignment, ACME challenges, renewal, and edge cases.
*
* Test Categories (15-18 tests):
* - Group A: Certificate Assignment (4 tests)
* - Group B: ACME Flow Integration (4 tests)
* - Group C: Certificate Lifecycle (4 tests)
* - Group D: Error Handling & Edge Cases (4 tests)
*
* API Endpoints:
* - GET/POST/DELETE /api/v1/certificates
* - GET/POST/PUT/DELETE /api/v1/proxy-hosts
* - GET/POST /api/v1/dns-providers
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import {
generateCertificate,
generateWildcardCertificate,
customCertificateMock,
selfSignedTestCert,
letsEncryptCertificate,
} from '../fixtures/certificates';
import { generateProxyHost } from '../fixtures/proxy-hosts';
import {
waitForToast,
waitForLoadingComplete,
waitForAPIResponse,
waitForModal,
waitForResourceInUI,
clickAndWaitForResponse,
} from '../utils/wait-helpers';
const DNS_PROVIDERS_API_PATTERN = /\/api\/v1\/dns-providers/;
const CERTIFICATES_API_PATTERN = /\/api\/v1\/certificates/;
async function navigateToDnsProviders(page: import('@playwright/test').Page): Promise<void> {
const providersResponse = waitForAPIResponse(page, DNS_PROVIDERS_API_PATTERN);
await page.goto('/dns/providers');
await providersResponse;
await waitForLoadingComplete(page);
}
async function navigateToCertificates(page: import('@playwright/test').Page): Promise<void> {
const certsResponse = waitForAPIResponse(page, CERTIFICATES_API_PATTERN);
await page.goto('/certificates');
await certsResponse;
await waitForLoadingComplete(page);
}
/**
* Selectors for Certificate and Proxy Host pages
*/
const SELECTORS = {
// Certificate Page
certPageTitle: 'h1',
uploadCertButton: 'button:has-text("Upload Certificate"), button:has-text("Add Certificate")',
requestCertButton: 'button:has-text("Request Certificate")',
certTable: '[data-testid="certificate-table"], table',
certRow: '[data-testid="certificate-row"], tbody tr',
certDeleteBtn: '[data-testid="cert-delete-btn"], button[aria-label*="Delete"]',
certViewBtn: '[data-testid="cert-view-btn"], button[aria-label*="View"]',
// Proxy Host Page
proxyPageTitle: 'h1',
createProxyButton: 'button:has-text("Create Proxy Host"), button:has-text("Add Proxy Host")',
proxyTable: '[data-testid="proxy-host-table"], table',
proxyRow: '[data-testid="proxy-host-row"], tbody tr',
proxyEditBtn: '[data-testid="proxy-edit-btn"], button[aria-label*="Edit"]',
// Form Fields
domainInput: 'input[name="domains"], #domains, input[placeholder*="domain"]',
certTypeSelect: 'select[name="type"], #cert-type',
certSelectDropdown: '[data-testid="cert-select"], select[name="certificate_id"]',
certFileInput: 'input[type="file"][name="certificate"]',
keyFileInput: 'input[type="file"][name="privateKey"]',
forceSSLCheckbox: 'input[name="force_ssl"], input[type="checkbox"][id*="ssl"]',
// Dialog/Modal
confirmDialog: '[role="dialog"], [role="alertdialog"]',
confirmButton: 'button:has-text("Confirm"), button:has-text("Delete"), button:has-text("Yes")',
cancelButton: 'button:has-text("Cancel"), button:has-text("No")',
saveButton: 'button:has-text("Save"), button[type="submit"]',
// Status/State
loadingSkeleton: '[data-testid="loading-skeleton"], .loading',
certStatusBadge: '[data-testid="cert-status"], .badge',
expiryWarning: '[data-testid="expiry-warning"], .warning',
};
test.describe('Proxy + Certificate Integration', () => {
// ===========================================================================
// Group A: Certificate Assignment (4 tests)
// ===========================================================================
test.describe('Group A: Certificate Assignment', () => {
test('should assign custom certificate to proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create a proxy host first
const proxyInput = generateProxyHost({ scheme: 'https' });
let createdProxy: { domain: string };
await test.step('Create proxy host via API', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
scheme: 'https',
});
});
await test.step('Navigate to certificates page', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify certificates page loads', async () => {
const heading = page.locator(SELECTORS.certPageTitle).first();
await expect(heading).toContainText(/certificate/i);
});
await test.step('Navigate to proxy hosts and verify', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
test('should assign Let\'s Encrypt certificate to proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost({ scheme: 'https' });
let createdProxy: { domain: string };
await test.step('Create proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
scheme: 'https',
});
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
});
await test.step('Verify proxy host is visible with HTTPS scheme', async () => {
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
test('should display SSL status indicator on proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost({ scheme: 'https', forceSSL: true });
let createdProxy: { domain: string };
await test.step('Create HTTPS proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
scheme: 'https',
});
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
});
await test.step('Verify SSL indicator is shown', async () => {
const proxyRow = page.locator(SELECTORS.proxyRow).filter({
hasText: createdProxy.domain,
});
await expect(proxyRow).toBeVisible();
// Look for HTTPS or SSL indicator (lock icon, badge, etc.)
const sslIndicator = proxyRow.locator('svg[data-testid*="lock"], .ssl-indicator, [aria-label*="SSL"], [aria-label*="HTTPS"]');
// This may or may not be present depending on UI implementation
});
});
test('should unassign certificate from proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost({ scheme: 'http' });
let createdProxy: { domain: string };
await test.step('Create HTTP proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
scheme: 'http',
});
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
});
// ===========================================================================
// Group B: ACME Flow Integration (4 tests)
// ===========================================================================
test.describe('Group B: ACME Flow Integration', () => {
test('should trigger HTTP-01 challenge for new certificate request', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost({ scheme: 'https' });
await test.step('Create proxy host for ACME challenge', async () => {
await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
scheme: 'https',
});
});
await test.step('Navigate to certificates page', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify certificates page is accessible', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should handle DNS-01 challenge for wildcard certificate', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// DNS provider is required for DNS-01 challenges
await test.step('Create DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'manual',
name: 'Wildcard-DNS-Provider',
credentials: {},
});
});
await test.step('Navigate to certificates', async () => {
await navigateToCertificates(page);
});
await test.step('Verify page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should show ACME challenge status during certificate issuance', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates page', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify page structure', async () => {
// Check for certificate list or empty state
const content = page.locator('main, .content, [role="main"]').first();
await expect(content).toBeVisible();
});
});
test('should link DNS provider for automated DNS-01 challenges', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'cloudflare',
name: 'Cloudflare-DNS-Test',
credentials: {
api_token: 'test-token-placeholder',
},
});
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify DNS provider exists', async () => {
await waitForResourceInUI(page, /Cloudflare-DNS-Test/i);
});
});
});
// ===========================================================================
// Group C: Certificate Lifecycle (4 tests)
// ===========================================================================
test.describe('Group C: Certificate Lifecycle', () => {
test('should display certificate expiry warning', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates page', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify certificates page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should show certificate renewal option', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
// The renewal option would be visible on a Let's Encrypt certificate row
await test.step('Verify page is accessible', async () => {
const content = page.locator('main, .content').first();
await expect(content).toBeVisible();
});
});
test('should handle certificate deletion with proxy host fallback', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost({ scheme: 'http' });
let createdProxy: { domain: string };
await test.step('Create proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
});
await test.step('Verify proxy host works without certificate', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
test('should auto-renew expiring certificates', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
// This test verifies the auto-renewal configuration is visible
await test.step('Navigate to settings', async () => {
await page.goto('/settings');
await waitForLoadingComplete(page);
});
await test.step('Verify settings page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
});
// ===========================================================================
// Group D: Error Handling & Edge Cases (4 tests)
// ===========================================================================
test.describe('Group D: Error Handling & Edge Cases', () => {
test('should handle invalid certificate upload gracefully', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify page loads without errors', async () => {
const content = page.locator('main, .content').first();
await expect(content).toBeVisible();
});
});
test('should handle mismatched certificate and private key', async ({
page,
adminUser,
}) => {
await loginUser(page, adminUser);
await test.step('Navigate to certificates', async () => {
await page.goto('/certificates');
await waitForLoadingComplete(page);
});
await test.step('Verify certificates page', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should prevent assigning expired certificate to proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost();
let createdProxy: { domain: string };
await test.step('Create proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
test('should handle domain mismatch between certificate and proxy host', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const proxyInput = generateProxyHost();
let createdProxy: { domain: string };
await test.step('Create proxy host', async () => {
createdProxy = await testData.createProxyHost({
domain: proxyInput.domain,
forwardHost: proxyInput.forwardHost,
forwardPort: proxyInput.forwardPort,
});
});
await test.step('Navigate to proxy hosts', async () => {
await page.goto('/proxy-hosts');
await waitForLoadingComplete(page);
});
await test.step('Verify proxy host exists', async () => {
await expect(page.getByText(createdProxy.domain)).toBeVisible();
});
});
});
});
@@ -0,0 +1,429 @@
/**
* Proxy + DNS Provider Integration E2E Tests
*
* Tests for proxy host and DNS provider integration workflows.
* Covers DNS provider configuration, ACME DNS-01 challenges, and validation.
*
* Test Categories (10-12 tests):
* - Group A: DNS Provider Assignment (3 tests)
* - Group B: DNS Challenge Integration (4 tests)
* - Group C: Provider Management (3 tests)
*
* API Endpoints:
* - GET/POST/PUT/DELETE /api/v1/dns-providers
* - GET/POST/PUT/DELETE /api/v1/proxy-hosts
* - POST /api/v1/dns-providers/:id/test
*/
import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import { generateProxyHost } from '../fixtures/proxy-hosts';
import {
waitForLoadingComplete,
waitForAPIResponse,
waitForResourceInUI,
} from '../utils/wait-helpers';
/**
* DNS Provider types supported by the system
*/
type DNSProviderType = 'manual' | 'cloudflare' | 'route53' | 'webhook' | 'rfc2136';
async function getAuthToken(page: import('@playwright/test').Page): Promise<string> {
const storageState = await page.request.storageState();
const origins = Array.isArray(storageState.origins) ? storageState.origins : [];
for (const originEntry of origins) {
const localStorageEntries = Array.isArray(originEntry?.localStorage)
? originEntry.localStorage
: [];
const authEntry = localStorageEntries.find((entry) => entry.name === 'auth');
if (authEntry?.value) {
try {
const parsed = JSON.parse(authEntry.value) as { token?: string };
if (parsed?.token) {
return parsed.token;
}
} catch {
}
}
const tokenEntry = localStorageEntries.find(
(entry) => entry.name === 'token' || entry.name === 'charon_auth_token'
);
if (tokenEntry?.value) {
return tokenEntry.value;
}
}
return '';
}
function buildAuthHeaders(token: string): Record<string, string> | undefined {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
async function navigateToDnsProviders(page: import('@playwright/test').Page): Promise<void> {
const providersResponse = waitForAPIResponse(page, /\/api\/v1\/dns-providers/);
await page.goto('/dns/providers');
await providersResponse;
await waitForLoadingComplete(page);
}
async function navigateToCertificates(page: import('@playwright/test').Page): Promise<void> {
const certsResponse = waitForAPIResponse(page, /\/api\/v1\/certificates/);
await page.goto('/certificates');
await certsResponse;
await waitForLoadingComplete(page);
}
/**
* Selectors for DNS Provider and Proxy Host pages
*/
const SELECTORS = {
// DNS Provider Page
dnsPageTitle: 'h1',
createDnsButton: 'button:has-text("Create DNS Provider"), button:has-text("Add DNS Provider")',
dnsTable: '[data-testid="dns-provider-table"], table',
dnsRow: '[data-testid="dns-provider-row"], tbody tr',
dnsDeleteBtn: '[data-testid="dns-delete-btn"], button[aria-label*="Delete"]',
dnsEditBtn: '[data-testid="dns-edit-btn"], button[aria-label*="Edit"]',
dnsTestBtn: '[data-testid="dns-test-btn"], button:has-text("Test")',
// Proxy Host Page
proxyPageTitle: 'h1',
createProxyButton: 'button:has-text("Create Proxy Host"), button:has-text("Add Proxy Host")',
proxyTable: '[data-testid="proxy-host-table"], table',
proxyRow: '[data-testid="proxy-host-row"], tbody tr',
proxyEditBtn: '[data-testid="proxy-edit-btn"], button[aria-label*="Edit"]',
// Form Fields
dnsTypeSelect: 'select[name="type"], #dns-type, [data-testid="dns-type-select"]',
dnsNameInput: 'input[name="name"], #dns-name',
apiTokenInput: 'input[name="api_token"], #api-token',
apiKeyInput: 'input[name="api_key"], #api-key',
webhookUrlInput: 'input[name="webhook_url"], #webhook-url',
// Dialog/Modal
confirmDialog: '[role="dialog"], [role="alertdialog"]',
confirmButton: 'button:has-text("Confirm"), button:has-text("Delete"), button:has-text("Yes")',
cancelButton: 'button:has-text("Cancel"), button:has-text("No")',
saveButton: 'button:has-text("Save"), button[type="submit"]',
// Status/State
loadingSkeleton: '[data-testid="loading-skeleton"], .loading',
statusBadge: '[data-testid="status-badge"], .badge',
};
test.describe('Proxy + DNS Provider Integration', () => {
// ===========================================================================
// Group A: DNS Provider Assignment (3 tests)
// ===========================================================================
test.describe('Group A: DNS Provider Assignment', () => {
test('should create manual DNS provider successfully', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create manual DNS provider via API', async () => {
const { id, name } = await testData.createDNSProvider({
providerType: 'manual',
name: 'Manual-DNS-Test',
credentials: {},
});
expect(id).toBeTruthy();
});
await test.step('Navigate to DNS providers page', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify DNS provider appears in list', async () => {
await waitForResourceInUI(page, /Manual-DNS-Test/i);
});
});
test('should create Cloudflare DNS provider', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create Cloudflare DNS provider via API', async () => {
const { id, name } = await testData.createDNSProvider({
providerType: 'cloudflare',
name: 'Cloudflare-DNS-Test',
credentials: {
api_token: 'test-cloudflare-token-placeholder',
},
});
expect(id).toBeTruthy();
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify provider is listed', async () => {
await waitForResourceInUI(page, /Cloudflare-DNS-Test/i);
});
});
test('should assign DNS provider to wildcard certificate request', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'manual',
name: 'Wildcard-DNS-Provider',
credentials: {},
});
});
await test.step('Navigate to certificates page', async () => {
await navigateToCertificates(page);
});
await test.step('Verify certificates page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
});
// ===========================================================================
// Group B: DNS Challenge Integration (4 tests)
// ===========================================================================
test.describe('Group B: DNS Challenge Integration', () => {
test('should test DNS provider connectivity', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create DNS provider for testing', async () => {
await testData.createDNSProvider({
providerType: 'manual',
name: 'Connectivity-Test-DNS',
credentials: {},
});
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify DNS providers page loads', async () => {
await waitForResourceInUI(page, /Connectivity-Test-DNS/i);
});
});
test('should display DNS challenge instructions for manual provider', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create manual DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'manual',
name: 'Manual-Challenge-DNS',
credentials: {},
});
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify page content', async () => {
await waitForResourceInUI(page, /Manual-Challenge-DNS/i);
});
});
test('should handle DNS propagation delay gracefully', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'manual',
name: 'Propagation-Test-DNS',
credentials: {},
});
});
await test.step('Navigate to certificates', async () => {
await navigateToCertificates(page);
});
await test.step('Verify page loads', async () => {
const heading = page.locator('h1, h2').first();
await expect(heading).toBeVisible();
});
});
test('should support webhook-based DNS provider', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
await test.step('Create webhook DNS provider', async () => {
await testData.createDNSProvider({
providerType: 'webhook',
name: 'Webhook-DNS-Test',
credentials: {
create_url: 'https://example.com/webhook/create',
delete_url: 'https://example.com/webhook/delete',
},
});
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify provider in list', async () => {
await waitForResourceInUI(page, /Webhook-DNS-Test/i);
});
});
});
// ===========================================================================
// Group C: Provider Management (3 tests)
// ===========================================================================
test.describe('Group C: Provider Management', () => {
test('should update DNS provider credentials', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const { id: providerId } = await testData.createDNSProvider({
providerType: 'cloudflare',
name: 'Update-Credentials-DNS',
credentials: {
api_token: 'initial-token',
},
});
const updatedName = 'Update-Credentials-DNS-Updated';
await test.step('Update provider credentials via API', async () => {
const token = await getAuthToken(page);
expect(token).toBeTruthy();
const response = await page.request.put(`/api/v1/dns-providers/${providerId}`, {
data: {
provider_type: 'cloudflare',
name: updatedName,
credentials: {
api_token: 'updated-token',
},
},
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify updated provider', async () => {
await waitForResourceInUI(page, updatedName);
});
});
test('should delete DNS provider with confirmation', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
const { id: providerId } = await testData.createDNSProvider({
providerType: 'manual',
name: 'Delete-Test-DNS',
credentials: {},
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify provider exists before deletion', async () => {
await waitForResourceInUI(page, /Delete-Test-DNS/i);
});
await test.step('Delete provider via API', async () => {
const token = await getAuthToken(page);
const response = await page.request.delete(`/api/v1/dns-providers/${providerId}`, {
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
});
await test.step('Verify provider removed from list', async () => {
await navigateToDnsProviders(page);
await expect(page.getByText(/Delete-Test-DNS/i)).toHaveCount(0);
});
});
test('should list all configured DNS providers', async ({
page,
adminUser,
testData,
}) => {
await loginUser(page, adminUser);
// Create multiple DNS providers
await testData.createDNSProvider({
providerType: 'manual',
name: 'List-Test-DNS-1',
credentials: {},
});
await testData.createDNSProvider({
providerType: 'cloudflare',
name: 'List-Test-DNS-2',
credentials: { api_token: 'test-token' },
});
await test.step('Navigate to DNS providers', async () => {
await navigateToDnsProviders(page);
});
await test.step('Verify providers list', async () => {
await waitForResourceInUI(page, /List-Test-DNS-1/i);
await waitForResourceInUI(page, /List-Test-DNS-2/i);
});
await test.step('Verify API returns providers', async () => {
const token = await getAuthToken(page);
const response = await page.request.get('/api/v1/dns-providers', {
headers: buildAuthHeaders(token),
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const providers = data.providers || data.items || data;
expect(Array.isArray(providers)).toBe(true);
});
});
});
});