first commit

This commit is contained in:
2026-05-22 11:46:43 +02:00
commit 2ebd8fb16b
29 changed files with 3356 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markWrite } from '../../helpers/manual';
import { attachWriteResult } from '../../helpers/writeResult';
const DOCS_URL = process.env.DOCS_URL ?? `${BASE_URL}/docs`;
const REPO_URL = process.env.REPO_URL ?? `${BASE_URL}/downloads`;
test.describe('core-documentation', () => {
test('core-documentation-write-release_notes_feature_changes_and_known_issues', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Navigate to documentation portal, extract and record Release Notes, feature changes, and known issues' });
const captured: Record<string, unknown> = {};
await test.step('Navigate to Release Notes page', async () => {
await page.goto(REPO_URL);
const rnLink = page.locator('a').filter({ hasText: /release[\s_-]?notes?/i }).first();
await rnLink.click();
await page.waitForLoadState('domcontentloaded');
});
await test.step('Extract release notes full content', async () => {
captured['url'] = page.url();
captured['title'] = await page.title();
captured['content'] = await page.locator('main, article, .content, body').first().textContent();
});
await test.step('Identify new features section', async () => {
const featuresEl = page.locator('h2, h3').filter({ hasText: /new feature|what.?s new/i }).first();
if (await featuresEl.count() > 0) {
captured['featuresSection'] = await featuresEl.textContent();
}
});
await test.step('Identify known issues section', async () => {
const issuesEl = page.locator('h2, h3').filter({ hasText: /known issue|limitation/i }).first();
if (await issuesEl.count() > 0) {
captured['knownIssuesSection'] = await issuesEl.textContent();
}
});
await test.step('Attach captured data to report', async () => {
await attachWriteResult(testInfo, captured, 'release-notes');
});
expect(captured['content']).toBeTruthy();
});
test('core-documentation-check-accuracy_installation_and_configuration_guide', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify accuracy of the Installation and Configuration Guide against actual system behaviour' });
markManual('Requires expert human review: a QA engineer must follow the guide step-by-step on a clean system and validate that each instruction produces the documented result');
// Manual steps:
// 1. Open the Installation and Configuration Guide.
// 2. Provision a clean VM / bare-metal server.
// 3. Follow each installation step exactly as documented.
// 4. At each step, verify the actual outcome matches the documented outcome.
// 5. Note any discrepancy, screenshot or mark the failed step.
// 6. Document findings in the test run report.
});
test('core-documentation-check-accuracy_trouble_shooting_guide', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify the Troubleshooting Guide procedures are accurate and complete' });
markManual('Requires human expertise: each troubleshooting scenario must be reproduced and the documented resolution steps must be validated against real system behaviour');
// Manual steps:
// 1. List all troubleshooting scenarios in the guide.
// 2. For each scenario, reproduce the described problem condition on a test system.
// 3. Follow the documented resolution steps.
// 4. Confirm the issue is resolved as described.
// 5. Document any steps that are inaccurate, incomplete, or out of date.
});
test('core-documentation-write-changed_cli_configuration_commands', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Navigate to documentation and extract the list of changed CLI configuration commands for this release' });
const captured: Record<string, unknown> = {};
await test.step('Navigate to documentation portal', async () => {
await page.goto(DOCS_URL);
});
await test.step('Locate CLI reference or changelog section', async () => {
const cliLink = page.locator('a').filter({ hasText: /cli|command.?line|reference/i }).first();
if (await cliLink.count() > 0) {
await cliLink.click();
await page.waitForLoadState('domcontentloaded');
}
captured['url'] = page.url();
});
await test.step('Extract CLI commands listed on the page', async () => {
const codeBlocks = await page.locator('code, pre, .cli, .command').allTextContents();
captured['cliCommands'] = codeBlocks;
captured['totalCommandsFound'] = codeBlocks.length;
});
await test.step('Attach captured CLI data to report', async () => {
await attachWriteResult(testInfo, captured, 'cli-commands');
});
expect(Array.isArray(captured['cliCommands'])).toBe(true);
});
});
+152
View File
@@ -0,0 +1,152 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial, markWrite } from '../../helpers/manual';
import { attachWriteResult } from '../../helpers/writeResult';
const DOCS_URL = process.env.DOCS_URL ?? `${BASE_URL}/docs`;
test.describe('core-features', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('core-features-write-summary_of_each_new_feature_per_release', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Extract and record a summary of each new feature included in this release' });
const captured: Record<string, unknown> = {};
await test.step('Navigate to Release Notes / What\'s New section', async () => {
await page.goto(`${DOCS_URL}/release-notes`);
captured['url'] = page.url();
});
await test.step('Extract feature list items', async () => {
const features = await page
.locator('h2, h3, li, .feature-item')
.filter({ hasText: /feature|new|added|introduced/i })
.allTextContents();
captured['newFeatures'] = features;
captured['count'] = features.length;
});
await test.step('Attach feature summary to report', async () => {
await attachWriteResult(testInfo, captured, 'feature-summary');
});
expect(captured['count']).toBeGreaterThan(0);
});
test('core-features-check-integration_of_new_features_with_existing_call_processing_and_modules', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('UI configuration and module settings can be verified via admin portal; actual call-flow integration requires active telephony endpoints outside Playwright scope');
testInfo.annotations.push({ type: 'description', description: 'Verify new features integrate correctly with existing call processing and system modules' });
await test.step('Navigate to admin portal modules section', async () => {
await page.goto(`${BASE_URL}/admin/modules`);
});
await test.step('Verify modules list is visible and no error state is shown', async () => {
await expect(page.locator('body')).not.toContainText(/error|failed|unavailable/i);
});
await test.step('Verify new feature settings are accessible in the portal', async () => {
await expect(page.locator('main, .content').first()).toBeVisible();
});
// TODO_MANUAL: For each new feature, trigger a call flow that exercises it and
// verify the call completes without errors in the call log.
});
test('core-features-check-backward_compatibility_of_new_features', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Configuration import compatibility and settings migration can be verified via admin portal; functional backward compatibility with older endpoint firmware requires manual telephony testing');
testInfo.annotations.push({ type: 'description', description: 'Verify new features are backward-compatible with existing configurations and older endpoint firmware' });
await test.step('Import a backup from the previous version', async () => {
await page.goto(`${BASE_URL}/admin/backup`);
await expect(page.locator('body')).not.toContainText(/error/i);
});
await test.step('Verify configuration is shown without migration errors', async () => {
await expect(page.locator('.alert-danger, .error-banner').first()).toHaveCount(0);
});
// TODO_MANUAL: Register an older SIP endpoint (previous firmware) and verify it
// still registers and can complete calls without errors.
});
test('core-features-check-performance_and_resource_impact_assessment_of_new_features', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires dedicated SIP load generator (SIPp), performance monitoring infrastructure, and sustained load; cannot be executed with Playwright alone');
testInfo.annotations.push({ type: 'description', description: 'Assess CPU/memory/network resource impact of new features under representative call load' });
// Manual steps:
// 1. Establish a baseline resource measurement with current stable build.
// 2. Enable each new feature one at a time.
// 3. Run SIPp with a representative concurrent-call scenario (e.g., 100 simultaneous calls).
// 4. Capture CPU, RAM, and I/O metrics every 30 s for at least 10 minutes.
// 5. Compare metrics against the baseline.
// 6. Record any resource spike or degradation > 10% as a finding.
});
test('core-features-write-analysis_of_new_security_risks', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
markWrite();
markManual('Security risk analysis requires expert threat modelling, source code review, and human judgment; not automatable via browser automation');
testInfo.annotations.push({ type: 'description', description: 'Identify, analyse, and document new security risks introduced by features in this release' });
// Manual steps:
// 1. Obtain the diff / commit list for this release.
// 2. Review each new feature for new attack surfaces (new APIs, new ports, new auth flows).
// 3. Run OWASP ZAP or equivalent against new endpoints.
// 4. Document findings: risk level (Critical/High/Medium/Low), description, CVE if applicable.
// 5. Record results in the security risk register.
});
test('core-features-write-ui_ux_and_admin_portal_changes_validation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Capture and record all UI/UX changes in the admin portal for this release (screenshots + text)' });
const captured: Record<string, unknown> = {};
const navSections = ['dashboard', 'extensions', 'trunks', 'dialplans', 'settings', 'security', 'logs'];
await test.step('Iterate admin portal main sections and capture screenshots', async () => {
captured['sections'] = [];
for (const section of navSections) {
await page.goto(`${BASE_URL}/admin/${section}`);
const title = await page.title();
const screenshot = await page.screenshot({ fullPage: false });
await testInfo.attach(`screenshot-${section}`, { body: screenshot, contentType: 'image/png' });
(captured['sections'] as string[]).push(`${section}: ${title}`);
}
});
await test.step('Attach section inventory to report', async () => {
await attachWriteResult(testInfo, captured, 'ui-changes');
});
expect((captured['sections'] as string[]).length).toBeGreaterThan(0);
});
test('core-features-check-provisioning_changes_for_new_features', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify provisioning settings and workflows for new features are accessible and correctly displayed in the admin portal' });
await test.step('Navigate to provisioning settings', async () => {
await page.goto(`${BASE_URL}/admin/provisioning`);
});
await test.step('Verify provisioning page loads without errors', async () => {
await expect(page.locator('body')).not.toContainText(/500|error|not found/i);
});
await test.step('Verify provisioning options are visible', async () => {
await expect(page.locator('main, .content, form').first()).toBeVisible();
});
});
});
+110
View File
@@ -0,0 +1,110 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial } from '../../helpers/manual';
test.describe('core-installation', () => {
test('core-installation-check-fresh_installation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires provisioning a clean VM/bare-metal host, mounting the installation ISO/OVA, and completing the setup wizard; this cannot be driven by Playwright alone');
testInfo.annotations.push({ type: 'description', description: 'Verify a complete fresh installation from scratch succeeds on each supported hypervisor' });
// Manual steps:
// 1. Provision a clean VM with the minimum required specs (vCPU, RAM, Disk).
// 2. Mount the installation ISO or import the OVA template.
// 3. Power on and follow the installation wizard / first-boot script.
// 4. Verify installation completes without errors (exit code 0 / no red messages).
// 5. Verify the admin portal is accessible at the configured IP:port.
// 6. Record: installation time, any warnings, hypervisor used.
});
test('core-installation-check-post_install_service_status_and_port_verification', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Service status dashboard in admin portal can be verified by Playwright; TCP port verification requires nmap or netstat from outside/inside the host — run that step as a separate CLI check in CI');
testInfo.annotations.push({ type: 'description', description: 'Verify all required services are running and expected ports are open after a fresh installation' });
await test.step('Navigate to System → Service Status', async () => {
await login(page);
await page.goto(`${BASE_URL}/admin/system/status`);
});
await test.step('Verify core services are shown as running', async () => {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/running|active|online/i);
});
await test.step('Verify no service is shown as failed or stopped', async () => {
const failedEl = page.locator('.status-failed, .status-stopped, [data-status="failed"]');
await expect(failedEl).toHaveCount(0);
});
// TODO_MANUAL: From a network-adjacent host run:
// nmap -p 443,80,5060,5061,8443 <pbx-ip>
// and verify all expected ports are OPEN.
});
test('core-installation-check-initial_configuration', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify the initial configuration wizard completes successfully and all mandatory settings are applied' });
await test.step('Navigate to admin portal — expect initial wizard on first run', async () => {
await page.goto(BASE_URL);
});
await test.step('Fill in System Name', async () => {
const sysNameInput = page.locator('[name="system_name"],[name="pbxname"],[id="system-name"]').first();
if (await sysNameInput.count() > 0) {
await sysNameInput.fill('PBX-Test-Instance');
}
});
await test.step('Fill in Administrator email', async () => {
const emailInput = page.locator('[name="admin_email"],[type="email"]').first();
if (await emailInput.count() > 0) {
await emailInput.fill('admin@test.local');
}
});
await test.step('Complete wizard and verify success message', async () => {
const nextBtn = page.locator('button:has-text("Next"),button:has-text("Finish"),button:has-text("Save")').first();
if (await nextBtn.count() > 0) {
await nextBtn.click();
await expect(page.locator('body')).not.toContainText(/error|failed/i);
}
});
await test.step('Verify admin portal dashboard is accessible after initial config', async () => {
await login(page);
await expect(page.locator('body')).not.toContainText(/wizard|setup required/i);
});
});
test('core-installation-check-license_activation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify license activation completes successfully and licensed features are unlocked' });
await test.step('Login to admin portal', async () => {
await login(page);
});
await test.step('Navigate to System → License', async () => {
await page.goto(`${BASE_URL}/admin/system/license`);
});
await test.step('Verify license page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify license status is shown', async () => {
const licenseInfo = page.locator('.license-status, .license-info, [data-field="license"]').first();
await expect(licenseInfo).toBeVisible();
});
await test.step('Verify license is active (not expired or invalid)', async () => {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).not.toMatch(/expired|invalid license|not activated/i);
});
});
});
+72
View File
@@ -0,0 +1,72 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
test.describe('core-interoperability', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('core-interoperability-check-addm', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify ADDM (Automatic Discovery and Dependency Mapping) integration is configured and reporting data correctly' });
await test.step('Navigate to Integrations → ADDM', async () => {
await page.goto(`${BASE_URL}/admin/integrations/addm`);
});
await test.step('Verify ADDM integration page loads without errors', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify ADDM connection status is shown', async () => {
const status = page.locator('.connection-status, .addm-status, [data-field="addm-status"]').first();
await expect(status).toBeVisible();
});
await test.step('Verify ADDM last discovery timestamp is recent', async () => {
const tsEl = page.locator('[data-field="last-discovery"],[class*="timestamp"],[class*="last-sync"]').first();
if (await tsEl.count() > 0) {
const tsText = await tsEl.textContent() ?? '';
expect(tsText.trim()).not.toBe('');
}
});
await test.step('Verify no ADDM errors are reported', async () => {
await expect(page.locator('.alert-danger, .error-badge').filter({ hasText: /addm/i })).toHaveCount(0);
});
});
test('core-interoperability-check-siem', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify SIEM (Security Information and Event Management) integration is configured and forwarding security events' });
await test.step('Navigate to Integrations → SIEM', async () => {
await page.goto(`${BASE_URL}/admin/integrations/siem`);
});
await test.step('Verify SIEM integration page loads without errors', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify SIEM destination (syslog/SIEM server) is configured', async () => {
const destField = page.locator('[name*="siem"],[name*="syslog"],[id*="siem"],[id*="syslog"]').first();
if (await destField.count() > 0) {
const value = await destField.inputValue();
expect(value.trim()).not.toBe('');
}
});
await test.step('Verify SIEM event forwarding is enabled', async () => {
const enableToggle = page.locator('[name*="siem_enabled"],[id*="siem-enable"],[data-field="siem-enabled"]').first();
if (await enableToggle.count() > 0) {
await expect(enableToggle).toBeChecked();
}
});
await test.step('Verify SIEM connection status shows no errors', async () => {
await expect(page.locator('.siem-status, .connection-status').filter({ hasText: /error|fail/i })).toHaveCount(0);
});
});
});
+115
View File
@@ -0,0 +1,115 @@
import { test, expect } from '@playwright/test';
const REPO_URL = process.env.REPO_URL ?? `${process.env.BASE_URL ?? 'https://pbx.local:4443'}/downloads`;
test.describe('core-repository', () => {
test.use({ ignoreHTTPSErrors: true });
test('core-repository-check-presence_of_release_notes_and_changelog', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify Release Notes and Changelog are present in the software repository' });
await test.step('Navigate to the software repository / download portal', async () => {
await page.goto(REPO_URL);
});
await test.step('Verify Release Notes file or link is visible', async () => {
const link = page.locator('a, li, td').filter({ hasText: /release[\s_-]?notes?/i });
await expect(link.first()).toBeVisible();
});
await test.step('Verify Changelog file or link is visible', async () => {
const link = page.locator('a, li, td').filter({ hasText: /change[\s_-]?log/i });
await expect(link.first()).toBeVisible();
});
});
test('core-repository-check-presence_of_trouble_shooting_guide', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify Troubleshooting Guide is present in the repository' });
await test.step('Navigate to the documentation / download portal', async () => {
await page.goto(REPO_URL);
});
await test.step('Verify Troubleshooting Guide link or file is present', async () => {
const link = page.locator('a, li, td').filter({ hasText: /troubleshoot/i });
await expect(link.first()).toBeVisible();
});
});
test('core-repository-check-presence_of_all_mandatory_core-documentation_files', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify all mandatory core documentation files are listed in the repository' });
const mandatoryDocs: [string, RegExp][] = [
['Release Notes', /release[\s_-]?notes?/i],
['Installation Guide', /install/i],
['Configuration Guide', /configur/i],
['Troubleshooting Guide', /troubleshoot/i],
['Changelog', /change[\s_-]?log/i],
];
await test.step('Navigate to the documentation portal', async () => {
await page.goto(REPO_URL);
});
for (const [name, pattern] of mandatoryDocs) {
await test.step(`Verify "${name}" is present`, async () => {
const el = page.locator('a, li, td').filter({ hasText: pattern });
await expect(el.first()).toBeVisible();
});
}
});
test('core-repository-check-presence_of_main_software_version_all_hypervisors', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify main software version packages are available for all supported hypervisors (VMware, Hyper-V, KVM, etc.)' });
const hypervisors = ['vmware', 'vsphere', 'hyper-v', 'hyperv', 'kvm', 'proxmox', 'xen'];
await test.step('Navigate to the download portal', async () => {
await page.goto(REPO_URL);
});
await test.step('Verify at least one hypervisor package is listed', async () => {
const bodyText = (await page.locator('body').textContent())?.toLowerCase() ?? '';
const found = hypervisors.some((hv) => bodyText.includes(hv));
expect(found, `Expected at least one hypervisor entry (${hypervisors.join(', ')})`).toBe(true);
});
});
test('core-repository-check-presence_of_patch_version_and_its_compatibility', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify patch version packages and compatibility matrices are present' });
await test.step('Navigate to the download portal', async () => {
await page.goto(REPO_URL);
});
await test.step('Verify patch / compatibility information is present', async () => {
const el = page.locator('body').filter({ hasText: /patch|hotfix|compatibility|compat/i });
await expect(el).toBeVisible();
});
});
test('core-repository-check-presence_of_companion_tools_and_supporting_scripts', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify companion tools and supporting scripts are available in the repository' });
await test.step('Navigate to the download portal', async () => {
await page.goto(REPO_URL);
});
await test.step('Verify tools / scripts section is present', async () => {
const el = page.locator('a, li, td, h2, h3').filter({ hasText: /tool|script|util|companion|support/i });
await expect(el.first()).toBeVisible();
});
});
});
+254
View File
@@ -0,0 +1,254 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial, markWrite } from '../../helpers/manual';
import { attachWriteResult } from '../../helpers/writeResult';
test.describe('core-security', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('core-security-write-strong_password_policy', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Navigate to security settings and record the current password policy configuration' });
const captured: Record<string, unknown> = {};
await test.step('Navigate to Security → Password Policy', async () => {
await page.goto(`${BASE_URL}/admin/security/password-policy`);
});
await test.step('Extract policy fields', async () => {
const fields = ['minimum length', 'complexity', 'expiry', 'history', 'lockout'];
for (const field of fields) {
const el = page.locator(`[name*="password"], input, select`).filter({ hasText: new RegExp(field, 'i') });
if (await el.count() > 0) {
captured[field] = await el.first().inputValue().catch(() => 'n/a');
}
}
captured['pageText'] = await page.locator('main, form, .settings-panel').first().textContent();
});
await test.step('Attach password policy record', async () => {
await attachWriteResult(testInfo, captured, 'password-policy');
});
expect(captured['pageText']).toBeTruthy();
});
test('core-security-check-sip_tls_and_srtp_encryption_enabled_by_default', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify SIP TLS and SRTP media encryption are enabled by default in a fresh installation' });
await test.step('Navigate to SIP / Security settings', async () => {
await page.goto(`${BASE_URL}/admin/security/sip`);
});
await test.step('Verify SIP TLS is enabled', async () => {
const tlsToggle = page.locator('[name*="tls"],[id*="tls"],[data-field*="tls"]').first();
await expect(tlsToggle).toBeChecked();
});
await test.step('Verify SRTP is enabled', async () => {
const srtpToggle = page.locator('[name*="srtp"],[id*="srtp"],[data-field*="srtp"]').first();
await expect(srtpToggle).toBeChecked();
});
});
test('core-security-check-ip_access_control_lists', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify IP Access Control Lists section is accessible and correctly configured' });
await test.step('Navigate to Security → IP ACL / Firewall', async () => {
await page.goto(`${BASE_URL}/admin/security/acl`);
});
await test.step('Verify ACL section loads without errors', async () => {
await expect(page.locator('body')).not.toContainText(/500|unexpected error/i);
});
await test.step('Verify ACL rules list is displayed', async () => {
await expect(page.locator('table, .acl-list, .rule-list, .ip-list').first()).toBeVisible();
});
await test.step('Verify at least one allow/deny rule exists', async () => {
const rules = page.locator('tr, .acl-entry').filter({ hasText: /allow|deny|permit|block/i });
await expect(rules.first()).toBeVisible();
});
});
test('core-security-check-unnecessary_services_and_ports_are_disabled', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Service status can be checked via admin portal; comprehensive port scanning requires nmap from an external host — integrate nmap into CI pipeline separately');
testInfo.annotations.push({ type: 'description', description: 'Verify unnecessary services are disabled and only required ports are open' });
await test.step('Navigate to Services / System Status panel', async () => {
await page.goto(`${BASE_URL}/admin/system/services`);
});
await test.step('Verify listed services show only expected running state', async () => {
await expect(page.locator('body')).not.toContainText(/ftp|telnet|rsh|rlogin/i);
});
await test.step('Verify remote management uses SSH (not Telnet)', async () => {
await expect(page.locator('body')).not.toContainText(/telnet.*enabled|telnet.*active/i);
});
// TODO_MANUAL: From an external host, run:
// nmap -p- --open <pbx-ip>
// and verify that only the expected ports (e.g., 443, 5060, 5061, 10000-20000/udp) are open.
});
test('core-security-check-security_event_logging_and_audit_trail_is_active', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify security event logging and audit trail are active and recording events' });
await test.step('Navigate to Security → Audit Log', async () => {
await page.goto(`${BASE_URL}/admin/security/audit`);
});
await test.step('Verify audit log page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify at least one audit entry is visible', async () => {
await expect(page.locator('table, .log-list, .audit-list, .event-list').first()).toBeVisible();
});
await test.step('Trigger a login event and verify it appears in the log', async () => {
await page.goto(`${BASE_URL}/admin/security/audit?filter=login`);
const logEntry = page.locator('td, .log-entry').filter({ hasText: /login|authentication|auth/i });
await expect(logEntry.first()).toBeVisible();
});
});
test('core-security-check-auto_enrollment_certificate_management_process', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify auto-enrollment certificate management (ACME/Let\'s Encrypt or internal CA) is configured and functional' });
await test.step('Navigate to Security → Certificates', async () => {
await page.goto(`${BASE_URL}/admin/security/certificates`);
});
await test.step('Verify certificate management page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify active certificate is displayed', async () => {
const certInfo = page.locator('.cert-info, .certificate-item, table').first();
await expect(certInfo).toBeVisible();
});
await test.step('Verify certificate has a valid expiry date in the future', async () => {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/valid|expir|expires|auto|renew/i);
});
});
test('core-security-check-known_vulnerable_dependencies_in_release_artifacts', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('CVE scanning is automated with Trivy/OWASP-DC integrated in CI; Playwright verifies the scan report page in the portal. Actual scanning runs outside this browser test.');
testInfo.annotations.push({ type: 'description', description: 'Verify release artifacts do not contain known vulnerable dependencies (CVE scan via Trivy / OWASP Dependency-Check)' });
await test.step('Navigate to Security → Vulnerability Report (if available in portal)', async () => {
await page.goto(`${BASE_URL}/admin/security/vulnerabilities`);
});
await test.step('Verify vulnerability report section loads', async () => {
await expect(page.locator('body')).not.toContainText(/500/i);
});
await test.step('Verify no Critical or High severity CVEs are listed as unresolved', async () => {
const criticals = page.locator('.severity-critical, [data-severity="critical"], td').filter({ hasText: /critical.*unresolved|unresolved.*critical/i });
await expect(criticals).toHaveCount(0);
});
// TODO_MANUAL: Run `trivy image <release-image>` or OWASP Dependency-Check on release artifacts
// and assert exit code 0 (no Critical/High findings).
});
test('core-security-check-previous_security_findings_have_been_remediated_before_release', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires access to security issue tracker (Jira/GitHub Security Advisories) and cross-referencing resolved CVEs with the release changelog; human judgment required to confirm completeness');
testInfo.annotations.push({ type: 'description', description: 'Verify all previously identified security findings are remediated before this release ships' });
// Manual steps:
// 1. Pull the list of open security findings from the issue tracker (label: security, milestone: this release).
// 2. For each finding, verify the fix commit is included in this release tag.
// 3. Re-test each finding to confirm it is no longer reproducible.
// 4. Obtain written confirmation from the security lead.
// 5. Update finding status to "Resolved" in the tracker.
});
test('core-security-check-obtain_formal_security_team_sign_off_for_the_new_version', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('This is a governance gate requiring a human decision and formal sign-off document; cannot be automated');
testInfo.annotations.push({ type: 'description', description: 'Obtain formal written sign-off from the Security team approving the release version' });
// Manual steps:
// 1. Compile the Security Test Summary report.
// 2. Submit to the Security team for review at least 3 business days before release.
// 3. Security team reviews findings, risk acceptance, and remediation completeness.
// 4. Security lead signs the release approval document.
// 5. Store signed document in the release artefacts folder.
});
test('core-security-check-verify_role_based_access_control_and_least_privilege_principle', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify RBAC roles are correctly defined and least-privilege principle is enforced in the admin portal' });
await test.step('Navigate to Users → Roles & Permissions', async () => {
await page.goto(`${BASE_URL}/admin/users/roles`);
});
await test.step('Verify roles list is visible', async () => {
await expect(page.locator('table, .role-list, .roles-panel').first()).toBeVisible();
});
await test.step('Verify at least Admin and Read-Only roles exist', async () => {
const bodyText = (await page.locator('body').textContent())?.toLowerCase() ?? '';
expect(bodyText).toMatch(/admin|administrator/i);
expect(bodyText).toMatch(/read.?only|viewer|readonly/i);
});
await test.step('Verify a read-only user cannot access admin settings', async () => {
// This step validates the UI hides restricted controls for non-admin roles
await page.goto(`${BASE_URL}/admin/users/roles`);
const adminOnlyEl = page.locator('[data-role="admin"], [data-permission="admin-only"]');
await expect(adminOnlyEl.first()).toBeVisible();
});
});
test('core-security-check-encryption_of_sensitive_data_at_rest_and_in_transit', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('TLS/HTTPS enforcement and encryption settings can be verified via admin portal; database-level at-rest encryption requires file-system access or CLI commands outside Playwright scope');
testInfo.annotations.push({ type: 'description', description: 'Verify sensitive data is encrypted at rest (DB encryption) and in transit (HTTPS/TLS)' });
await test.step('Verify admin portal is served over HTTPS', async () => {
expect(BASE_URL).toMatch(/^https:\/\//);
await page.goto(BASE_URL);
expect(page.url()).toMatch(/^https:\/\//);
});
await test.step('Navigate to Security → Encryption settings', async () => {
await page.goto(`${BASE_URL}/admin/security/encryption`);
});
await test.step('Verify encryption settings page is accessible', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
// TODO_MANUAL: SSH into the server and verify:
// - DB files are stored on an encrypted volume or DB-level encryption is enabled.
// - `openssl s_client -connect <host>:443` shows TLS 1.2+ and strong cipher suite.
});
});
+121
View File
@@ -0,0 +1,121 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial } from '../../helpers/manual';
test.describe('core-upgrade', () => {
test('core-upgrade-check-in_place_upgrade_from_previous_major_version', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires a running instance of the previous major version, access to the new release package, and execution of the upgrade procedure; needs dedicated test environment');
testInfo.annotations.push({ type: 'description', description: 'Verify in-place upgrade from the previous major version completes successfully without data loss' });
// Manual steps:
// 1. Take a VM snapshot of the previous-version system as rollback point.
// 2. Upload / stage the new release package on the system.
// 3. Execute the upgrade command (e.g., `pbx-upgrade --release X.Y.Z`).
// 4. Monitor upgrade logs for errors.
// 5. Verify the system comes back online at the new version.
// 6. Check admin portal version string matches the target release.
// 7. Verify a sample set of configuration objects (extensions, trunks) survived the upgrade.
});
test('core-upgrade-check-configuration_and_database_migration_validation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Post-upgrade UI configuration checks are automated; DB schema integrity validation requires direct DB access (psql/mysql CLI) outside Playwright scope');
testInfo.annotations.push({ type: 'description', description: 'Verify configuration objects and database content migrate correctly after upgrade' });
await test.step('Login to admin portal after upgrade', async () => {
await login(page);
});
await test.step('Verify extensions list is intact', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
await expect(page.locator('table tbody tr, .extension-item').first()).toBeVisible();
});
await test.step('Verify trunk configurations are present', async () => {
await page.goto(`${BASE_URL}/admin/trunks`);
await expect(page.locator('table tbody tr, .trunk-item').first()).toBeVisible();
});
await test.step('Verify dial plan rules are present', async () => {
await page.goto(`${BASE_URL}/admin/dialplans`);
await expect(page.locator('body')).not.toContainText(/no.*rule|empty/i);
});
// TODO_MANUAL: SSH into the server and run:
// psql -U pbx -c '\dt' (or equivalent for your DB)
// Verify all expected tables are present and row counts match pre-upgrade counts.
});
test('core-upgrade-check-schema_upgrade_and_data_integrity_check', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires direct database access (psql/mysql) and schema comparison tooling; DB schema is not exposed via the admin portal');
testInfo.annotations.push({ type: 'description', description: 'Verify the database schema is correctly upgraded and all data maintains integrity after migration' });
// Manual steps:
// 1. Before upgrade: dump schema with `pg_dump --schema-only` and row counts.
// 2. After upgrade: dump new schema and compare with expected schema for new version.
// 3. Verify: no unexpected table drops, all new columns added, indexes intact.
// 4. Run data integrity checks: FK constraints, NULL columns for required fields.
// 5. Compare row counts for critical tables (extensions, cdr, trunks).
// 6. Document any discrepancies as issues.
});
test('core-upgrade-check-rollback_procedure_execution_and_verification', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Rollback requires either reverting a VM snapshot or downgrading packages on the OS — both require physical/hypervisor-level access and cannot be driven by Playwright');
testInfo.annotations.push({ type: 'description', description: 'Verify the rollback procedure successfully reverts the system to the previous version' });
// Manual steps:
// 1. Use the VM snapshot taken before upgrade OR follow documented package rollback procedure.
// 2. Execute rollback (snapshot revert or `pbx-upgrade --rollback`).
// 3. Monitor rollback logs for errors.
// 4. Verify the system boots at the previous version.
// 5. Verify admin portal is accessible and shows the previous version number.
// 6. Verify a sample call can be completed (if safe to test in this environment).
// 7. Record rollback duration and outcome.
});
test('core-upgrade-check-post_upgrade_regression_of_core_features', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Run automated regression checks on core portal features after upgrade to detect any regressions' });
await test.step('Login and verify portal is accessible', async () => {
await login(page);
await expect(page.locator('body')).toBeVisible();
});
const sections = [
{ name: 'Extensions', path: '/admin/extensions' },
{ name: 'Trunks', path: '/admin/trunks' },
{ name: 'Dial Plans', path: '/admin/dialplans' },
{ name: 'Users', path: '/admin/users' },
{ name: 'Security', path: '/admin/security' },
{ name: 'Logs', path: '/admin/logs' },
];
for (const section of sections) {
await test.step(`Verify "${section.name}" section loads without HTTP errors`, async () => {
const response = await page.goto(`${BASE_URL}${section.path}`);
expect(response?.status()).toBeLessThan(500);
await expect(page.locator('body')).not.toContainText(/unexpected error|500 internal/i);
});
}
});
test('core-upgrade-check-zero_downtime_upgrade_where_supported', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires active call sessions monitored during the upgrade window and concurrent upgrade execution on the server; calls cannot be established or monitored by Playwright');
testInfo.annotations.push({ type: 'description', description: 'Verify zero-downtime upgrade keeps the service available throughout the upgrade process' });
// Manual steps:
// 1. Establish 5+ active SIP calls before starting the upgrade.
// 2. Begin the rolling upgrade procedure.
// 3. Monitor portal availability with a ping/health-check script every 5 s.
// 4. Monitor active calls — none should drop.
// 5. Verify: no HTTP 5xx responses during upgrade, all calls survive, upgrade completes.
// 6. Record: downtime (should be 0), call drop count, upgrade duration.
});
});
+139
View File
@@ -0,0 +1,139 @@
import { test, expect } from '@playwright/test';
import { BASE_URL } from '../../helpers/auth';
import { markManual, markPartial, markWrite } from '../../helpers/manual';
import { attachWriteResult } from '../../helpers/writeResult';
const REPO_URL = process.env.REPO_URL ?? `${BASE_URL}/downloads`;
test.describe('core-virtualization', () => {
test('core-virtualization-write-supported_hypervisors_deployment', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
markWrite();
testInfo.annotations.push({ type: 'description', description: 'Record the list of supported hypervisors and their deployment methods from official documentation' });
const captured: Record<string, unknown> = {};
await test.step('Navigate to download / compatibility portal', async () => {
await page.goto(REPO_URL);
captured['url'] = page.url();
});
await test.step('Extract hypervisor entries from the page', async () => {
const items = await page.locator('td, li, .hypervisor-item, .platform-item').allTextContents();
const hypervisors = items.filter((t) =>
/vmware|vsphere|hyper-v|hyperv|kvm|proxmox|xen|virtualbox|nutanix/i.test(t),
);
captured['hypervisors'] = hypervisors;
captured['count'] = hypervisors.length;
});
await test.step('Attach hypervisor list to report', async () => {
await attachWriteResult(testInfo, captured, 'hypervisors');
});
expect(captured['count']).toBeGreaterThan(0);
});
test('core-virtualization-write-guest_agent_integration', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
markWrite();
markManual('Requires direct access to hypervisor console (VMware vCenter, Hyper-V Manager, KVM virsh) to inspect guest agent status; not accessible via web admin portal');
testInfo.annotations.push({ type: 'description', description: 'Record guest agent integration status and version for each supported hypervisor' });
// Manual steps:
// 1. For VMware: open vCenter → VM → Summary → VMware Tools status.
// 2. For Hyper-V: open Hyper-V Manager → VM → Integration Services.
// 3. For KVM/QEMU: run `virsh qemu-agent-command <vm> '{"execute":"guest-info"}'`.
// 4. For each hypervisor, record: agent version, status (running/stopped), capabilities.
// 5. Attach findings to this test.
});
test('core-virtualization-write-resource_allocation_vcpu_vram_vdisk', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
markWrite();
markManual('Hypervisor management plane access required (vCenter API, Hyper-V WMI, libvirt) to read vCPU/vRAM/vDisk allocations; these are not exposed in the PBX admin portal');
testInfo.annotations.push({ type: 'description', description: 'Record vCPU, vRAM, and vDisk resource allocation for each supported deployment template' });
// Manual steps:
// 1. Open the OVA/OVF template or deployment documentation.
// 2. For each supported size (Small/Medium/Large/XL), record: vCPUs, RAM (GB), Disk (GB).
// 3. Cross-reference with minimum requirements in the Installation Guide.
// 4. Verify live VM allocations match the template using hypervisor console.
// 5. Record findings in the test report.
});
test('core-virtualization-write-virtual_networking_configuration', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
markWrite();
markManual('Virtual switch and VLAN configuration lives in the hypervisor management plane (VMware DVS, Hyper-V Virtual Switch, OVS on KVM); not accessible via PBX admin portal');
testInfo.annotations.push({ type: 'description', description: 'Record virtual networking configuration: virtual switches, VLANs, NIC assignments' });
// Manual steps:
// 1. In the hypervisor, inspect the virtual switch assigned to the PBX VM.
// 2. Record: vSwitch name, uplink adapter, MTU, VLAN tagging mode.
// 3. Verify the PBX NIC is on the correct port group / VLAN.
// 4. Check that management and voice VLANs are separated if applicable.
// 5. Record configuration in this test's findings.
});
test('core-virtualization-check-live_migration_and_active_call_preservation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires HA infrastructure with ≥ 2 hypervisor hosts, active SIP call sessions during migration, and simultaneous call-state monitoring; cannot be performed by Playwright');
testInfo.annotations.push({ type: 'description', description: 'Verify VM live migration (vMotion / Live Migration) preserves active calls without interruption' });
// Manual steps:
// 1. Establish 5+ active SIP calls between extensions.
// 2. Trigger live migration of the PBX VM to a second hypervisor host.
// 3. Monitor active calls in the PBX portal during migration.
// 4. Verify: zero call drops, no audio interruption > 200 ms, migration completes < 30 s.
// 5. Record migration duration and call preservation result.
});
test('core-virtualization-check-snapshot_backup_restore_and_consistency', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Post-restore consistency checks (admin portal loads, config intact, services running) can be automated; the snapshot/restore operation itself requires hypervisor API or manual action');
testInfo.annotations.push({ type: 'description', description: 'Verify VM snapshot and restore operations maintain full system consistency' });
// TODO_MANUAL: Use hypervisor API or console to create a VM snapshot, modify config,
// then restore the snapshot before running the automated steps below.
await test.step('Navigate to admin portal after restore and verify it loads', async () => {
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await expect(page.locator('body')).toBeVisible();
});
await test.step('Verify no error banners are displayed after restore', async () => {
await expect(page.locator('.alert-danger, .error, [class*="error"]').first()).toHaveCount(0);
});
await test.step('Verify core services are shown as running in the dashboard', async () => {
await page.goto(`${BASE_URL}/admin/dashboard`);
await expect(page.locator('body')).not.toContainText(/stopped|offline|unavailable/i);
});
});
test('core-virtualization-check-hypervisor_level_high_availability', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires physical HA cluster with multiple hypervisor hosts; host failure must be simulated manually (power off host, network isolation); not achievable via Playwright');
testInfo.annotations.push({ type: 'description', description: 'Verify hypervisor-level HA (VMware HA, Hyper-V Failover Cluster) restarts the PBX VM after a host failure' });
// Manual steps:
// 1. Confirm HA is enabled on the cluster hosting the PBX VM.
// 2. Note the current host where the PBX VM is running.
// 3. Simulate host failure (power off host or isolate network).
// 4. Measure time to VM restart on a surviving host.
// 5. Verify PBX admin portal becomes accessible within the defined RTO (e.g., < 5 min).
// 6. Record: failover time, target host, any lost registrations.
});
test('core-virtualization-write-storage_thin_vs_thick_provisioning', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
markWrite();
markManual('Storage provisioning details (thin/thick/lazy-zeroed) are visible only in the hypervisor storage management interface; not exposed in the PBX admin portal');
testInfo.annotations.push({ type: 'description', description: 'Record storage provisioning type (thin vs thick) and observed performance implications for each supported hypervisor' });
// Manual steps:
// 1. In vCenter/Hyper-V/KVM, open the PBX VM disk properties.
// 2. Record: provisioning type, allocated size, used size.
// 3. Run a disk I/O benchmark (fio) on both thin and thick provisioned deployments.
// 4. Record: sequential read/write IOPS, latency (ms).
// 5. Document recommended provisioning type based on findings.
});
});
+66
View File
@@ -0,0 +1,66 @@
import { test } from '@playwright/test';
import { markManual } from '../../helpers/manual';
// High-Availability tests require multi-node cluster infrastructure,
// ability to simulate failures, and real-time call monitoring.
// None can be driven by Playwright alone.
test.describe('pbx-availability', () => {
test('pbx-availability-check-active_passive_or_active_active_failover', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires HA cluster with ≥ 2 nodes; failover must be triggered by stopping the active node — cannot be orchestrated by a browser automation tool');
testInfo.annotations.push({ type: 'description', description: 'Verify Active-Passive or Active-Active failover transitions correctly when the active node becomes unavailable' });
// Manual steps:
// 1. Confirm cluster is healthy in admin portal → HA Status.
// 2. Note which node is currently Active.
// 3. Stop PBX service on the Active node (or power off the node).
// 4. Measure time for cluster to promote Passive → Active.
// 5. Verify admin portal becomes accessible on the new Active node within the defined RTO (e.g., < 30 s).
// 6. Verify SIP registrations survive or re-register after failover.
// 7. Record: failover time, re-registration count, any calls lost.
});
test('pbx-availability-check-database_replication_and_sync_integrity', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires HA DB cluster (PostgreSQL streaming replication, MySQL Galera, etc.); verifying replication lag and data consistency requires direct DB access');
testInfo.annotations.push({ type: 'description', description: 'Verify database replication between HA nodes maintains consistency and acceptable replication lag' });
// Manual steps:
// 1. On the Primary DB node, run: `SELECT * FROM pg_stat_replication;` (PostgreSQL).
// 2. Verify replication lag < 1 s.
// 3. Insert a test record on Primary; verify it appears on Secondary within 5 s.
// 4. Simulate primary failure; verify Secondary auto-promotes.
// 5. After re-adding Primary as replica, verify replication resumes.
// 6. Record: replication method, lag values, failover and re-join times.
});
test('pbx-availability-check-automatic_failover_and_call_preservation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires active call sessions during a node failure event and simultaneous call-state monitoring from both the phone and the PBX dashboard; not achievable via browser automation');
testInfo.annotations.push({ type: 'description', description: 'Verify automatic failover preserves active calls (no audio interruption > 200 ms, no call drops) when a node fails' });
// Manual steps:
// 1. Establish 10 active calls across multiple extensions.
// 2. Trigger failover (stop Active node service).
// 3. Listen on all 10 calls throughout failover.
// 4. Verify: ≤ 0 calls dropped, audio interruption ≤ 200 ms.
// 5. After failover, verify CDR records exist for all 10 calls.
// 6. Record: failover time, calls dropped, audio gap duration per call.
});
test('pbx-availability-check-geographic_redundancy_and_geo_distribution', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires multi-site infrastructure across geographic locations, WAN link simulation (or real WAN), and geo-failover testing — far beyond browser automation scope');
testInfo.annotations.push({ type: 'description', description: 'Verify geo-distributed deployment handles site failover and routes calls to the surviving site without service interruption' });
// Manual steps:
// 1. Deploy PBX nodes in at least 2 geographically distinct sites (or simulate with VMs in separate networks).
// 2. Verify cross-site SIP registration and call routing function normally.
// 3. Simulate site failure (isolate Site A network).
// 4. Verify: SIP clients in Site A re-register to Site B within RTO.
// 5. Make calls through Site B; verify audio quality is acceptable (MOS > 3.5).
// 6. Restore Site A; verify split-brain is avoided and cluster converges.
// 7. Record: failover time, re-registration count, call success rate during transition.
});
});
+112
View File
@@ -0,0 +1,112 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markPartial } from '../../helpers/manual';
test.describe('pbx-calls-security', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('pbx-calls-security-check-srtp_sdes_or_dtls_media_encryption', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Encryption configuration can be verified via admin portal; confirming that live RTP traffic is actually encrypted requires packet capture (Wireshark) during an active call');
testInfo.annotations.push({ type: 'description', description: 'Verify SRTP with SDES or DTLS-SRTP is enabled for media encryption on calls' });
await test.step('Navigate to Security → SIP / Media Encryption', async () => {
await page.goto(`${BASE_URL}/admin/security/media-encryption`);
});
await test.step('Verify media encryption settings page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify SRTP is enabled', async () => {
const srtpToggle = page.locator('[name*="srtp"],[id*="srtp"],[data-field*="srtp"]').first();
if (await srtpToggle.count() > 0) {
await expect(srtpToggle).toBeChecked();
} else {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/srtp.*enabled|srtp.*on|media.*encrypt/i);
}
});
await test.step('Verify encryption mode is SDES or DTLS', async () => {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/sdes|dtls/i);
});
// TODO_MANUAL: While an active SRTP call is running, capture RTP packets with Wireshark.
// Verify: no RTP packets are visible in plain text (audio payload is encrypted).
// Confirm SDP shows a=crypto (SDES) or a=fingerprint (DTLS) attribute in INVITE/200 OK.
});
test('pbx-calls-security-check-acl_ip_whitelisting_and_rate_limiting', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('ACL rule configuration can be verified via admin portal; testing that the rules actually block traffic requires attempting connections from non-whitelisted IPs — a network-level functional test');
testInfo.annotations.push({ type: 'description', description: 'Verify ACL IP whitelisting rules and SIP rate limiting are configured and enforced' });
await test.step('Navigate to Security → ACL / Firewall', async () => {
await page.goto(`${BASE_URL}/admin/security/acl`);
});
await test.step('Verify ACL page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify at least one IP whitelist rule exists', async () => {
const rules = page.locator('tr, .acl-entry').filter({ hasText: /allow|whitelist|permit/i });
await expect(rules.first()).toBeVisible();
});
await test.step('Navigate to rate limiting settings', async () => {
await page.goto(`${BASE_URL}/admin/security/rate-limiting`);
});
await test.step('Verify rate limit configuration is shown', async () => {
await expect(page.locator('[name*="rate"],[id*="rate"],[class*="rate-limit"]').first()).toBeVisible();
});
// TODO_MANUAL: From a non-whitelisted IP address, attempt a SIP REGISTER.
// Verify the attempt is rejected (403 Forbidden or no response).
// Send > rate-limit threshold requests/sec; verify subsequent requests are blocked.
});
test('pbx-calls-security-check-toll_fraud_prevention_rules_and_alerting', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify toll fraud prevention rules are configured and alert notifications are enabled' });
await test.step('Navigate to Security → Toll Fraud Prevention', async () => {
await page.goto(`${BASE_URL}/admin/security/toll-fraud`);
});
await test.step('Verify toll fraud page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify call limit thresholds are configured', async () => {
const limitField = page.locator('[name*="max_calls"],[name*="call_limit"],[id*="threshold"]').first();
if (await limitField.count() > 0) {
const value = await limitField.inputValue();
expect(parseInt(value, 10)).toBeGreaterThan(0);
}
});
await test.step('Verify alerting email is configured', async () => {
const emailField = page.locator('[name*="alert_email"],[name*="notification_email"]').first();
if (await emailField.count() > 0) {
const value = await emailField.inputValue();
expect(value).toMatch(/@/);
}
});
await test.step('Verify geographic restrictions or blocked country codes are visible', async () => {
const geoSection = page.locator('.geo-restriction, .country-block, [data-section="geo"]').first();
if (await geoSection.count() > 0) {
await expect(geoSection).toBeVisible();
}
});
});
});
+103
View File
@@ -0,0 +1,103 @@
import { test } from '@playwright/test';
import { markManual } from '../../helpers/manual';
// All tests in this group require active SIP endpoints, PSTN access, or real telephony
// infrastructure. They cannot be driven by Playwright + MCP alone.
test.describe('pbx-calls', () => {
test('pbx-calls-check-internal_extension_to_extension_audio_call', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires two active SIP endpoints (phones or softphones) and human verification of two-way audio quality');
testInfo.annotations.push({ type: 'description', description: 'Verify an internal extension-to-extension audio call connects, audio is bi-directional, and CDR is generated' });
// Manual steps:
// 1. Register extension 1001 on Phone A, extension 1002 on Phone B.
// 2. From Phone A, dial 1002.
// 3. Verify Phone B rings and shows Caller ID of 1001.
// 4. Answer on Phone B. Verify two-way audio on both handsets.
// 5. Hang up. Verify both phones return to idle state.
// 6. In admin portal → Logs → CDR, verify a CDR record is created with correct extension, duration, direction.
// 7. Record: call setup time (ms), audio quality (MOS if tool available), CDR accuracy.
});
test('pbx-calls-check-inbound_call_from_sip_trunk_pstn', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires live SIP trunk connectivity to PSTN, a real DID number, and a phone to receive the call');
testInfo.annotations.push({ type: 'description', description: 'Verify an inbound call from a SIP trunk (PSTN) routes correctly to the configured destination' });
// Manual steps:
// 1. Ensure a SIP trunk is registered and active in admin portal.
// 2. Configure an inbound route for the DID to an extension or ring group.
// 3. Dial the DID from an external PSTN number.
// 4. Verify the call rings the configured destination.
// 5. Answer and verify two-way audio.
// 6. Check CDR shows inbound direction, correct DID, and trunk name.
});
test('pbx-calls-check-outbound_call_to_sip_trunk_pstn_with_caller_id', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires live SIP trunk, ability to dial a PSTN number, and verification of Caller ID as seen by the receiving party');
testInfo.annotations.push({ type: 'description', description: 'Verify outbound call via SIP trunk completes and presents the correct Caller ID on the receiving end' });
// Manual steps:
// 1. From a registered extension, dial an outbound number (e.g., 9 + number).
// 2. Verify the call routes through the configured outbound rule and SIP trunk.
// 3. Answer on the receiving PSTN phone. Verify Caller ID matches configured outbound CID.
// 4. Confirm two-way audio.
// 5. Review CDR for outbound direction, trunk used, and Caller ID value.
});
test('pbx-calls-check-caller_id_presentation_restriction_and_pai', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires active call sessions and SIP packet capture (Wireshark) to verify P-Asserted-Identity header values in SIP INVITE messages');
testInfo.annotations.push({ type: 'description', description: 'Verify Caller ID presentation, Anonymous restriction (CLIR), and P-Asserted-Identity (PAI) header in SIP INVITE' });
// Manual steps:
// 1. Test CLIR: dial *67 + number from a registered extension; verify receiving end sees Anonymous/Unknown.
// 2. Capture SIP traffic with Wireshark during a call with PAI configured.
// 3. Inspect INVITE packets: verify P-Asserted-Identity header contains the correct identity.
// 4. Test outbound CID override: verify configured CID appears in From and PAI headers.
// 5. Document: PAI value observed, From header value, CLIR behaviour.
});
test('pbx-calls-check-call_hold_resume_and_music_on_hold', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires an active two-party call; Hold/Resume is a mid-call feature and Music on Hold requires audio monitoring with actual SIP phone hardware');
testInfo.annotations.push({ type: 'description', description: 'Verify call hold, resume, and Music on Hold (MoH) playback work correctly' });
// Manual steps:
// 1. Establish a call between two extensions.
// 2. Press Hold on Phone A. Verify Phone B hears Music on Hold.
// 3. Verify hold music is the configured MoH file (recognisable audio).
// 4. Press Resume on Phone A. Verify two-way audio is restored instantly.
// 5. Record: hold activation time, MoH audio quality, resume time.
});
test('pbx-calls-check-call_waiting_notification_and_switching', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires concurrent call sessions on the same extension with a physical or softphone capable of handling call-waiting signals');
testInfo.annotations.push({ type: 'description', description: 'Verify call waiting notification is presented and the user can switch between active and waiting calls' });
// Manual steps:
// 1. Establish an active call on extension 1001.
// 2. While 1001 is in-call, have a second caller dial 1001.
// 3. Verify extension 1001 hears call-waiting tone / sees notification.
// 4. Switch to the waiting call (flash / hold key). Verify first caller is put on hold.
// 5. Switch back. Verify first call is resumed and second caller is on hold.
// 6. Hang up both calls. Record behaviour.
});
test('pbx-calls-check-three_way_conference_and_multi_party_bridge', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires three or more active SIP endpoints and a working conference bridge; audio verification for all participants requires human listeners');
testInfo.annotations.push({ type: 'description', description: 'Verify three-way conference call and multi-party conference bridge functionality' });
// Manual steps:
// 1. Extension 1001 calls 1002. Both are in active audio call.
// 2. 1001 initiates a transfer/conference to 1003.
// 3. Verify all three parties hear each other clearly.
// 4. Verify admin portal shows the conference with 3 participants.
// 5. One participant drops. Verify remaining two continue their call.
// 6. Test via dedicated conference room: dial conference number, enter PIN, verify all hear each other.
});
});
+52
View File
@@ -0,0 +1,52 @@
import { test } from '@playwright/test';
import { markManual } from '../../helpers/manual';
// Codec-level tests require active SIP calls with specific codec configurations
// and packet capture / audio analysis tools. None can be driven by Playwright alone.
test.describe('pbx-codecs', () => {
test('pbx-codecs-check-codec_negotiation_g711_g729_opus_g722', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires SIP call traffic with codec-constrained endpoints and Wireshark packet capture to verify SDP offer/answer negotiation for each codec');
testInfo.annotations.push({ type: 'description', description: 'Verify successful codec negotiation for G.711 (alaw/ulaw), G.729, Opus, and G.722 between endpoints' });
// Manual steps:
// 1. In admin portal, configure a test extension to use only G.711 alaw. Make a call and capture SDP.
// Verify SDP answer contains only pcma/8000.
// 2. Repeat with G.729: verify annexb, SDP shows g729/8000.
// 3. Repeat with Opus: verify SDP shows opus/48000/2.
// 4. Repeat with G.722: verify SDP shows G722/8000.
// 5. Test codec fallback: configure preferred codec that remote doesn't support; verify negotiation falls back to next common codec.
// 6. Record Wireshark SDP captures for each scenario.
});
test('pbx-codecs-check-dtmf_transmission_rfc2833_inband_sip_info', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires active call sessions with DTMF generation (physical or software phone), packet capture for RFC2833, and IVR system to verify DTMF reception');
testInfo.annotations.push({ type: 'description', description: 'Verify DTMF digit transmission via RFC2833, in-band audio, and SIP INFO methods are handled correctly' });
// Manual steps:
// 1. Configure extension for RFC2833 DTMF. Call an IVR. Press digits 0-9,*,#.
// Wireshark: verify RTP telephone-event packets are sent for each digit.
// IVR: verify digits are recognised and routing occurs correctly.
// 2. Configure extension for in-band DTMF. Repeat dial-through IVR.
// Verify IVR recognises tones (confirm with IVR logs).
// 3. Configure extension for SIP INFO DTMF. Repeat.
// Wireshark: verify SIP INFO messages contain correct DTMF signal field.
// 4. Record: each method used, packets observed, IVR recognition accuracy.
});
test('pbx-codecs-check-t38_fax_passthrough_and_error_correction', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires fax hardware (physical fax machine or fax server, e.g., HylaFAX) and T.38 stack support; cannot be tested with a browser');
testInfo.annotations.push({ type: 'description', description: 'Verify T.38 fax passthrough and error correction (ECM) work correctly over the PBX' });
// Manual steps:
// 1. Configure a SIP trunk with T.38 support enabled.
// 2. Send a fax from a fax machine / software fax client to a DID number routed to a fax extension.
// 3. Verify the fax is received completely and without page corruption.
// 4. Check Wireshark: verify T.38 re-INVITE is sent when fax tone is detected (CNG → T.38 switchover).
// 5. Test T.38 ECM: induce packet loss (network emulator), verify re-transmission corrects errors.
// 6. Record: pages sent/received, error count, ECM effectiveness.
});
});
+143
View File
@@ -0,0 +1,143 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import * as path from 'path';
import * as fs from 'fs';
test.describe('pbx-configuration', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('pbx-configuration-check-initial_system_setup_and_network_settings', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify initial PBX system setup and network settings are correctly configured' });
await test.step('Navigate to Settings → Network', async () => {
await page.goto(`${BASE_URL}/admin/settings/network`);
});
await test.step('Verify network settings page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify hostname/FQDN field is populated', async () => {
const fqdnInput = page.locator('[name*="hostname"],[name*="fqdn"],[id*="hostname"],[id*="fqdn"]').first();
if (await fqdnInput.count() > 0) {
const value = await fqdnInput.inputValue();
expect(value.trim()).not.toBe('');
}
});
await test.step('Verify IP address field is populated', async () => {
const ipInput = page.locator('[name*="ip_address"],[name*="local_ip"],[id*="ip-address"]').first();
if (await ipInput.count() > 0) {
const value = await ipInput.inputValue();
expect(value).toMatch(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
}
});
await test.step('Verify DNS server field is populated', async () => {
const dnsInput = page.locator('[name*="dns"],[id*="dns"]').first();
if (await dnsInput.count() > 0) {
const value = await dnsInput.inputValue();
expect(value.trim()).not.toBe('');
}
});
});
test('pbx-configuration-check-dialplan_route_and_outbound_rule_creation', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify creation and validation of dial plan rules and outbound routes' });
const testRuleName = `autotest-rule-${Date.now()}`;
await test.step('Navigate to Dial Plans / Outbound Routes', async () => {
await page.goto(`${BASE_URL}/admin/dialplans/outbound`);
});
await test.step('Create a new outbound rule', async () => {
await page.locator('button:has-text("Add"),button:has-text("New"),a:has-text("Add Rule")').first().click();
await page.locator('[name*="name"],[id*="rule-name"]').first().fill(testRuleName);
await page.locator('[name*="prefix"],[name*="pattern"],[id*="pattern"]').first().fill('9.');
await page.locator('button:has-text("Save"),button[type="submit"]').first().click();
});
await test.step('Verify new rule appears in the list', async () => {
await page.goto(`${BASE_URL}/admin/dialplans/outbound`);
await expect(page.locator('table, .rule-list').filter({ hasText: testRuleName })).toBeVisible();
});
await test.step('Edit the rule and verify save succeeds', async () => {
await page.locator('tr, .rule-item').filter({ hasText: testRuleName }).locator('a:has-text("Edit"),button:has-text("Edit")').first().click();
await page.locator('button:has-text("Save"),button[type="submit"]').first().click();
await expect(page.locator('.alert-success, .success-banner, [class*="success"]').first()).toBeVisible();
});
await test.step('Delete the test rule', async () => {
await page.goto(`${BASE_URL}/admin/dialplans/outbound`);
await page.locator('tr, .rule-item').filter({ hasText: testRuleName }).locator('button:has-text("Delete"),a:has-text("Delete")').first().click();
await page.locator('button:has-text("Confirm"),button:has-text("Yes")').first().click();
await expect(page.locator('body')).not.toContainText(testRuleName);
});
});
test('pbx-configuration-check-backup_restore_full_system_configuration', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify full system configuration backup and restore operations work correctly' });
await test.step('Navigate to Backup & Restore', async () => {
await page.goto(`${BASE_URL}/admin/backup`);
});
await test.step('Trigger a backup and wait for completion', async () => {
await page.locator('button:has-text("Create Backup"),button:has-text("Backup Now")').first().click();
await expect(page.locator('.backup-status, .progress, [data-status="complete"]').first()).toBeVisible({ timeout: 60_000 });
});
await test.step('Verify backup file appears in the backup list', async () => {
const backupList = page.locator('table, .backup-list');
await expect(backupList.first()).toBeVisible();
const rows = await backupList.locator('tr, .backup-item').count();
expect(rows).toBeGreaterThan(0);
});
await test.step('Verify restore button is available for the latest backup', async () => {
const restoreBtn = page.locator('button:has-text("Restore"),a:has-text("Restore")').first();
await expect(restoreBtn).toBeVisible();
});
});
test('pbx-configuration-check-bulk_csv_import_export_of_settings', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify bulk CSV import and export of settings (extensions, contacts, etc.)' });
const csvContent = 'extension,name,email\n5001,Test User One,test1@test.local\n5002,Test User Two,test2@test.local';
const tmpCsvPath = path.join(process.cwd(), 'test-results', 'bulk-import-test.csv');
await test.step('Navigate to Bulk Import / Export section', async () => {
fs.mkdirSync(path.dirname(tmpCsvPath), { recursive: true });
fs.writeFileSync(tmpCsvPath, csvContent, 'utf-8');
await page.goto(`${BASE_URL}/admin/extensions/import`);
});
await test.step('Verify import page loads', async () => {
await expect(page.locator('input[type="file"], .dropzone, .upload-area').first()).toBeVisible();
});
await test.step('Navigate to export section', async () => {
await page.goto(`${BASE_URL}/admin/extensions/export`);
});
await test.step('Trigger CSV export and verify download is initiated', async () => {
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 20_000 }),
page.locator('button:has-text("Export"),a:has-text("Export CSV"),a:has-text("Download")').first().click(),
]);
expect(download.suggestedFilename()).toMatch(/\.csv$/i);
});
});
});
+103
View File
@@ -0,0 +1,103 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial } from '../../helpers/manual';
const TEST_EXT_NUMBER = '8801';
const TEST_EXT_NAME = 'Autotest Extension';
test.describe('pbx-extensions', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('pbx-extensions-check-create_modify_delete_sip_extensions', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify full CRUD lifecycle (Create, Modify, Delete) for SIP extensions in the admin portal' });
await test.step('Navigate to Extensions section', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
});
await test.step('Create a new SIP extension', async () => {
await page.locator('button:has-text("Add"),button:has-text("New Extension"),a:has-text("Add Extension")').first().click();
await page.locator('[name*="extension"],[name*="ext_number"],[id*="extension"]').first().fill(TEST_EXT_NUMBER);
await page.locator('[name*="name"],[name*="display_name"],[id*="name"]').first().fill(TEST_EXT_NAME);
await page.locator('[name*="password"],[id*="sip-password"]').first().fill('Test@12345!');
await page.locator('button:has-text("Save"),button[type="submit"]').first().click();
});
await test.step('Verify new extension appears in the list', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
await expect(page.locator('table, .extension-list').filter({ hasText: TEST_EXT_NUMBER })).toBeVisible();
});
await test.step('Edit the extension — change display name', async () => {
await page.locator('tr, .extension-item').filter({ hasText: TEST_EXT_NUMBER }).locator('a:has-text("Edit"),button:has-text("Edit")').first().click();
const nameInput = page.locator('[name*="name"],[name*="display_name"]').first();
await nameInput.clear();
await nameInput.fill(`${TEST_EXT_NAME} Modified`);
await page.locator('button:has-text("Save"),button[type="submit"]').first().click();
});
await test.step('Verify modified name is persisted', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
await expect(page.locator('body')).toContainText(`${TEST_EXT_NAME} Modified`);
});
await test.step('Delete the test extension', async () => {
await page.locator('tr, .extension-item').filter({ hasText: TEST_EXT_NUMBER }).locator('button:has-text("Delete"),a:has-text("Delete")').first().click();
await page.locator('button:has-text("Confirm"),button:has-text("Yes"),button:has-text("Delete")').last().click();
});
await test.step('Verify extension is removed from the list', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
await expect(page.locator('body')).not.toContainText(TEST_EXT_NUMBER);
});
});
test('pbx-extensions-check-extension_registration_with_deskphones_softphones', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires physical SIP desk phone (e.g., Yealink, Polycom, Snom) or software SIP client (Zoiper, MicroSIP) — registration cannot be simulated by Playwright');
testInfo.annotations.push({ type: 'description', description: 'Verify a SIP extension registers successfully from a desk phone and from a softphone client' });
// Manual steps:
// 1. On a physical desk phone: enter PBX IP, extension number, and SIP password.
// 2. Verify phone shows "Registered" status on its LCD.
// 3. In admin portal → Extensions, verify the extension shows "Registered" status.
// 4. Repeat with a softphone (Zoiper or MicroSIP) on a laptop.
// 5. Record: phone model / firmware, registration time, portal status.
});
test('pbx-extensions-check-device_provisioning', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Provisioning profile creation and URL configuration can be verified via admin portal; actual device auto-provisioning requires a physical phone that fetches the config');
testInfo.annotations.push({ type: 'description', description: 'Verify automatic device provisioning: provisioning server URL accessible, profile generated correctly' });
await test.step('Navigate to Provisioning settings', async () => {
await page.goto(`${BASE_URL}/admin/provisioning`);
});
await test.step('Verify provisioning is enabled', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
await expect(page.locator('main, .content').first()).toBeVisible();
});
await test.step('Verify provisioning server URL is configured', async () => {
const urlField = page.locator('[name*="prov_url"],[name*="provisioning_url"],[id*="prov-url"]').first();
if (await urlField.count() > 0) {
const value = await urlField.inputValue();
expect(value).toMatch(/^https?:\/\//);
}
});
await test.step('Verify provisioning templates are listed for at least one phone brand', async () => {
const templates = page.locator('.template-item, .device-template, tr').filter({ hasText: /yealink|polycom|snom|grandstream|cisco|fanvil/i });
await expect(templates.first()).toBeVisible();
});
// TODO_MANUAL: Connect a physical SIP phone to the same network, configure it to fetch
// provisioning from the server URL, reboot, and verify it registers automatically.
});
});
+178
View File
@@ -0,0 +1,178 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markPartial } from '../../helpers/manual';
test.describe('pbx-logs', () => {
test.beforeEach(async ({ page }) => {
await login(page);
});
test('pbx-logs-check-cdr_generation_accuracy_completeness_and_export', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify CDR records are generated, contain all required fields, and can be exported to CSV/PDF' });
await test.step('Navigate to Reports → CDR', async () => {
await page.goto(`${BASE_URL}/admin/logs/cdr`);
});
await test.step('Verify CDR page loads without errors', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify CDR table is visible', async () => {
await expect(page.locator('table, .cdr-list, .report-table').first()).toBeVisible();
});
await test.step('Verify required CDR columns are present (start time, duration, from, to, status)', async () => {
const headerText = await page.locator('thead, .table-header').first().textContent() ?? '';
const requiredCols = ['time', 'duration', 'from', 'to', 'status'];
for (const col of requiredCols) {
expect(headerText.toLowerCase()).toContain(col);
}
});
await test.step('Trigger CSV export and verify download', async () => {
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 20_000 }),
page.locator('button:has-text("Export"),a:has-text("CSV"),a:has-text("Download")').first().click(),
]);
expect(download.suggestedFilename()).toMatch(/\.(csv|xlsx|pdf)$/i);
});
});
test('pbx-logs-check-real_time_active_call_and_registration_monitoring', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify real-time monitoring dashboard shows active calls and SIP registrations with auto-refresh' });
await test.step('Navigate to Monitoring → Active Calls', async () => {
await page.goto(`${BASE_URL}/admin/monitoring/active-calls`);
});
await test.step('Verify active calls panel loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
await expect(page.locator('table, .active-calls, .call-panel').first()).toBeVisible();
});
await test.step('Navigate to Monitoring → Registrations', async () => {
await page.goto(`${BASE_URL}/admin/monitoring/registrations`);
});
await test.step('Verify registrations panel loads', async () => {
await expect(page.locator('table, .registrations, .reg-panel').first()).toBeVisible();
});
await test.step('Verify auto-refresh indicator is present', async () => {
const autoRefresh = page.locator('[data-refresh],[data-auto-refresh],.refresh-indicator,.last-updated').first();
if (await autoRefresh.count() > 0) {
await expect(autoRefresh).toBeVisible();
}
});
});
test('pbx-logs-check-detailed_logging_levels_rotation_and_search', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify detailed logging levels can be configured, log rotation is set up, and log search/filter is functional' });
await test.step('Navigate to System → Logs', async () => {
await page.goto(`${BASE_URL}/admin/logs/system`);
});
await test.step('Verify log viewer loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
await expect(page.locator('.log-viewer, .log-content, pre, textarea').first()).toBeVisible();
});
await test.step('Navigate to log settings (level and rotation)', async () => {
await page.goto(`${BASE_URL}/admin/settings/logging`);
});
await test.step('Verify log level selector is available', async () => {
const levelSelect = page.locator('[name*="log_level"],[id*="log-level"],select').filter({ hasText: /debug|info|warn|error/i }).first();
await expect(levelSelect).toBeVisible();
});
await test.step('Verify log rotation settings are shown', async () => {
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/rotation|rotate|max.*size|keep.*days/i);
});
await test.step('Test log search / filter functionality', async () => {
await page.goto(`${BASE_URL}/admin/logs/system`);
const searchInput = page.locator('[name*="search"],[placeholder*="search"],[placeholder*="filter"]').first();
if (await searchInput.count() > 0) {
await searchInput.fill('error');
await page.keyboard.press('Enter');
await expect(page.locator('body')).not.toContainText(/500/i);
}
});
});
test('pbx-logs-check-threshold_alarms_cpu_channels_disk_memory', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify threshold-based alarms are configured for CPU, active channels, disk, and memory usage' });
await test.step('Navigate to Monitoring → Alarms / Thresholds', async () => {
await page.goto(`${BASE_URL}/admin/monitoring/alarms`);
});
await test.step('Verify alarms page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
const alarmTypes = ['cpu', 'memory', 'disk', 'channel'];
for (const alarmType of alarmTypes) {
await test.step(`Verify ${alarmType.toUpperCase()} threshold alarm is configurable`, async () => {
const alarmEl = page
.locator('tr, .alarm-item, .threshold-item, .metric-row')
.filter({ hasText: new RegExp(alarmType, 'i') });
if (await alarmEl.count() > 0) {
await expect(alarmEl.first()).toBeVisible();
}
});
}
await test.step('Verify notification email for alarms is configured', async () => {
await page.goto(`${BASE_URL}/admin/settings/notifications`);
const emailField = page.locator('[name*="alert_email"],[name*="notif_email"],[type="email"]').first();
if (await emailField.count() > 0) {
const value = await emailField.inputValue();
expect(value).toMatch(/@/);
}
});
});
test('pbx-logs-check-call_quality_metrics_mos_jitter_loss_per_call', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Quality metrics display and reporting UI can be automated; actual metric generation requires active call sessions with measurable network conditions (jitter, packet loss)');
testInfo.annotations.push({ type: 'description', description: 'Verify per-call quality metrics (MOS score, jitter, packet loss) are collected and displayed in the portal' });
await test.step('Navigate to Reports → Call Quality / QoS', async () => {
await page.goto(`${BASE_URL}/admin/reports/call-quality`);
});
await test.step('Verify call quality report page loads', async () => {
await expect(page.locator('body')).not.toContainText(/500|not found/i);
});
await test.step('Verify quality metrics columns are present (MOS, jitter, packet loss)', async () => {
const headerText = (await page.locator('thead, .table-header').first().textContent() ?? '').toLowerCase();
const hasQualityMetrics = /mos|jitter|packet.?loss|r.?value/.test(headerText);
if (!hasQualityMetrics) {
testInfo.annotations.push({ type: 'note', description: 'Quality metrics columns not found — may require active calls or a specific report period to populate' });
}
});
await test.step('Verify date range filter is available', async () => {
const dateFilter = page.locator('[name*="date"],[name*="range"],input[type="date"]').first();
await expect(dateFilter).toBeVisible();
});
// TODO_MANUAL: Establish calls with controlled network impairment (tc qdisc netem) to generate
// measurable jitter and packet loss; verify the portal records accurate MOS scores.
});
});
+81
View File
@@ -0,0 +1,81 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markPartial } from '../../helpers/manual';
test.describe('pbx-management', () => {
test('pbx-management-check-cli_management', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('CLI commands can be scripted via SSH automation (separate Node.js/shell step in CI); Playwright verifies the management web console if one exists. Full CLI coverage requires ssh2 integration alongside this test.');
testInfo.annotations.push({ type: 'description', description: 'Verify CLI management interface is accessible and key management commands execute correctly' });
await test.step('Navigate to admin portal — Web SSH / Console (if available)', async () => {
await login(page);
await page.goto(`${BASE_URL}/admin/console`);
});
await test.step('Verify web console loads or CLI section is accessible', async () => {
const pageText = await page.locator('body').textContent() ?? '';
const hasConsole = /console|terminal|ssh|cli/i.test(pageText);
// If no web console, note it — CLI testing must be done via separate SSH step
testInfo.annotations.push({
type: 'note',
description: hasConsole
? 'Web console found — proceed with command verification'
: 'No web console detected — CLI testing must be done via SSH in CI pipeline',
});
});
// TODO_MANUAL / TODO_SSH: Connect via SSH and verify these commands execute successfully:
// - `pbx-ctl status` → shows all services running
// - `pbx-ctl restart sip` → restarts SIP service without error
// - `pbx-cli show version` → shows correct version string
// - `pbx-cli show extensions` → lists all extensions
// - `pbx-cli show calls` → shows current active calls (empty if no calls)
});
test('pbx-management-check-gui_management', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'automated' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
testInfo.annotations.push({ type: 'description', description: 'Verify GUI admin portal is accessible, all main sections load correctly, and management actions are functional' });
await test.step('Login to admin portal', async () => {
await login(page);
});
const sections = [
{ name: 'Dashboard', path: '/admin/dashboard' },
{ name: 'Extensions', path: '/admin/extensions' },
{ name: 'Trunks', path: '/admin/trunks' },
{ name: 'Dial Plans', path: '/admin/dialplans' },
{ name: 'Conferences', path: '/admin/conferences' },
{ name: 'Ring Groups', path: '/admin/ring-groups' },
{ name: 'IVR', path: '/admin/ivr' },
{ name: 'Voicemail', path: '/admin/voicemail' },
{ name: 'Backup', path: '/admin/backup' },
{ name: 'Security', path: '/admin/security' },
{ name: 'Logs', path: '/admin/logs' },
{ name: 'Settings', path: '/admin/settings' },
];
for (const section of sections) {
await test.step(`Verify "${section.name}" section is accessible`, async () => {
const response = await page.goto(`${BASE_URL}${section.path}`);
expect(
response?.status(),
`Section "${section.name}" returned HTTP ${response?.status()}`,
).toBeLessThan(500);
await expect(page.locator('body')).not.toContainText(/500 internal server error/i);
});
}
await test.step('Verify navigation menu is displayed', async () => {
await page.goto(`${BASE_URL}/admin/dashboard`);
await expect(page.locator('nav, .sidebar, .menu').first()).toBeVisible();
});
await test.step('Verify user info / logout option is visible', async () => {
await expect(page.locator('body')).toContainText(/logout|log out|sign out|admin/i);
});
});
});
+51
View File
@@ -0,0 +1,51 @@
import { test } from '@playwright/test';
import { markManual } from '../../helpers/manual';
// Performance tests require SIP load generators, sustained telephony infrastructure,
// and extended monitoring. None can be executed by Playwright alone.
test.describe('pbx-performance', () => {
test('pbx-performance-check-cpu_memory_disk_io_utilization_under_sustained_load', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires SIPp load generator, server-side performance monitoring (Prometheus/Grafana or top/iostat), and a dedicated test environment with no other traffic');
testInfo.annotations.push({ type: 'description', description: 'Verify CPU, memory, and disk I/O remain within acceptable limits under sustained call load' });
// Manual steps:
// 1. Prepare SIPp scenario with target concurrent call count (e.g., 100, 200, 500 CPS).
// 2. Configure monitoring: collect CPU%, RAM%, disk I/O (iostat) every 10 s.
// 3. Run SIPp load for 30 minutes at target load.
// 4. Record peak and average CPU / RAM / disk read-write.
// 5. Verify: CPU < 80%, RAM < 90%, disk I/O latency < 20 ms.
// 6. Record all findings and compare against accepted SLA thresholds.
});
test('pbx-performance-check-long_duration_stability_72h_test', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('72-hour test requires dedicated test environment, SIPp continuous load, automated monitoring with alerting, and human review of metrics at intervals — cannot be managed by a browser automation tool');
testInfo.annotations.push({ type: 'description', description: 'Verify system stability under continuous call load over a 72-hour period without degradation, crashes, or memory leaks' });
// Manual steps:
// 1. Set up SIPp to run a steady call load (e.g., 50% max capacity) continuously.
// 2. Deploy monitoring: CPU, RAM, open file descriptors, active calls, call failure rate.
// 3. Set up alerts if: call failure rate > 1%, RAM > 90%, or any process restarts.
// 4. Run for 72 hours.
// 5. Review metrics at 24h, 48h, and 72h checkpoints.
// 6. Verify: no service restarts, no call failure spike, resource usage stable.
// 7. Document final metrics report.
});
test('pbx-performance-check-memory_leak_and_resource_cleanup_detection', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires hours/days of monitored operation with active call load and memory profiling tools (Valgrind, heap dumps, or OS-level monitoring) — not achievable via browser automation');
testInfo.annotations.push({ type: 'description', description: 'Detect memory leaks and verify proper resource cleanup over extended operation under call load' });
// Manual steps:
// 1. Record baseline memory (RSS, heap) of all PBX processes at t=0.
// 2. Run a call load for 4 hours (enough to surface leaks).
// 3. Monitor memory every 30 minutes: record process RSS, open FDs, thread count.
// 4. After load stops, monitor resource cleanup over 15 minutes.
// 5. Compare t=0 vs end baseline: memory growth > 10% is a potential leak.
// 6. If leak suspected, repeat with Valgrind / heap profiler for confirmation.
// 7. Document: memory trend graph, peak values, any confirmed leaks.
});
});
+127
View File
@@ -0,0 +1,127 @@
import { test, expect } from '@playwright/test';
import { login, BASE_URL } from '../../helpers/auth';
import { markManual, markPartial } from '../../helpers/manual';
test.describe('pbx-regression', () => {
test('pbx-regression-check-core_call_features_after_any_code_change', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Admin portal CRUD and configuration regression can be automated; actual call-flow regression (inbound/outbound/hold/transfer) requires active SIP endpoints outside Playwright scope');
testInfo.annotations.push({ type: 'description', description: 'Run regression checks on core call features after any code change to detect regressions' });
await test.step('Login and verify portal is accessible', async () => {
await login(page);
await expect(page.locator('body')).toBeVisible();
});
await test.step('Verify Extension CRUD functions are intact', async () => {
await page.goto(`${BASE_URL}/admin/extensions`);
await expect(page.locator('body')).not.toContainText(/500|unexpected error/i);
await expect(page.locator('table, .extension-list, .no-extensions').first()).toBeVisible();
});
await test.step('Verify Trunk list loads correctly', async () => {
await page.goto(`${BASE_URL}/admin/trunks`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
await test.step('Verify Dial Plan section is intact', async () => {
await page.goto(`${BASE_URL}/admin/dialplans`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
await test.step('Verify Voicemail settings load correctly', async () => {
await page.goto(`${BASE_URL}/admin/voicemail`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
await test.step('Verify IVR / Auto-Attendant section loads', async () => {
await page.goto(`${BASE_URL}/admin/ivr`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
// TODO_MANUAL: After each code merge, run SIP regression suite:
// - Internal call test (ext→ext)
// - Inbound DID test
// - Outbound call test
// - Hold/Resume, Transfer, Voicemail deposit/retrieval
});
test('pbx-regression-check-previously_fixed_bugs_and_edge_cases_reverification', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('UI-level bug fixes can be regression-tested via Playwright; telephony-layer or server-side bug fixes require SIP clients or SSH-based verification');
testInfo.annotations.push({ type: 'description', description: 'Verify all previously fixed bugs and known edge cases have not regressed in this release' });
const knownBugRoutes = [
// Add routes that previously had UI bugs — update this list per release
{ name: 'Extension edit page', path: '/admin/extensions' },
{ name: 'Trunk status display', path: '/admin/trunks' },
{ name: 'CDR export', path: '/admin/logs/cdr' },
{ name: 'User roles', path: '/admin/users/roles' },
];
await test.step('Login', async () => {
await login(page);
});
for (const route of knownBugRoutes) {
await test.step(`Verify "${route.name}" loads without regression`, async () => {
const response = await page.goto(`${BASE_URL}${route.path}`);
expect(response?.status()).toBeLessThan(500);
await expect(page.locator('body')).not.toContainText(/unexpected error|traceback|exception/i);
});
}
// TODO_MANUAL: For each closed bug ticket, execute the reproduction steps from the ticket
// and verify the bug is not reproducible on this release.
});
test('pbx-regression-check-performance_baselines_comparison_against_previous_version', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markManual('Requires SIPp load tests producing comparable metrics against the previous version baseline; statistical comparison and pass/fail determination require human or scripted analysis outside the browser');
testInfo.annotations.push({ type: 'description', description: 'Compare key performance metrics (concurrent calls, call setup time, resource usage) against the previous version baseline' });
// Manual steps:
// 1. Run the standard SIPp performance benchmark on the PREVIOUS version. Record: max CPS, P95 setup time, peak CPU.
// 2. Install NEW version. Run the identical SIPp benchmark.
// 3. Compare: max CPS diff must be ≤ 5% degradation; setup time P95 diff ≤ 10%.
// 4. If any metric degrades > threshold, file a performance regression bug.
// 5. Attach benchmark results (CSV + charts) to this test report.
});
test('pbx-regression-check-security_hardening_and_vulnerability_scan_baseline', async ({ page }, testInfo) => {
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
testInfo.annotations.push({ type: 'testType', description: 'check' });
markPartial('Security configuration checks (HTTPS, RBAC, encryption enabled) can be automated via admin portal; full vulnerability scan baseline requires external tools (OpenVAS, Nessus, Trivy) integrated into the CI pipeline');
testInfo.annotations.push({ type: 'description', description: 'Verify security hardening settings are intact and vulnerability scan result is no worse than the previous version baseline' });
await test.step('Login and navigate to security overview', async () => {
await login(page);
await page.goto(`${BASE_URL}/admin/security`);
});
await test.step('Verify HTTPS enforcement is active', async () => {
expect(page.url()).toMatch(/^https:\/\//);
});
await test.step('Verify RBAC settings page loads', async () => {
await page.goto(`${BASE_URL}/admin/users/roles`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
await test.step('Verify media encryption is still enabled after code change', async () => {
await page.goto(`${BASE_URL}/admin/security/media-encryption`);
const pageText = await page.locator('body').textContent() ?? '';
expect(pageText).toMatch(/srtp|tls/i);
});
await test.step('Verify audit logging is still active', async () => {
await page.goto(`${BASE_URL}/admin/security/audit`);
await expect(page.locator('body')).not.toContainText(/500/i);
});
// TODO_MANUAL / TODO_CI: Run `trivy image <new-release-image>` and compare CVE count
// against the previous version scan. New Critical/High CVEs = regression.
});
});