chore: git cache cleanup
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user