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