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
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(awk -F: '{sum += $2} END {print \"Total test\\(\\) calls in spec files:\", sum}')",
"Bash(awk -F: '{sum += $2} END {print \"Total tests:\", sum}')"
]
}
}
+15
View File
@@ -0,0 +1,15 @@
# Copy this file to .env.local and fill in your values
# .env.local is git-ignored
# Admin portal base URL (no trailing slash)
BASE_URL=https://pbx.local:4443
# Download / software repository portal URL
REPO_URL=https://repo.pbx.local/downloads
# Admin credentials
ADMIN_USER=admin
ADMIN_PASSWORD=changeme
# Optional: CI environment flag
CI=false
+36
View File
@@ -0,0 +1,36 @@
import { Page } from '@playwright/test';
const BASE_URL = process.env.BASE_URL ?? 'https://pbx.local:4443';
const ADMIN_USER = process.env.ADMIN_USER ?? 'admin';
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD ?? 'admin';
/** Navigates to the admin portal login page and authenticates. */
export async function login(page: Page, baseUrl = BASE_URL): Promise<void> {
await page.goto(`${baseUrl}/login`, { waitUntil: 'domcontentloaded' });
await page
.locator('[name="username"],[name="user"],#username,#user,input[type="text"]:first-of-type')
.first()
.fill(ADMIN_USER);
await page
.locator('[name="password"],#password,input[type="password"]')
.first()
.fill(ADMIN_PASSWORD);
await page
.locator('[type="submit"],button:has-text("Login"),button:has-text("Sign in"),button:has-text("Entrar")')
.first()
.click();
await page.waitForURL(/dashboard|home|index|admin|portal/, { timeout: 20_000 });
}
export async function logout(page: Page): Promise<void> {
await page
.locator('button:has-text("Logout"),a:has-text("Logout"),a:has-text("Log out"),[aria-label="Logout"]')
.first()
.click();
}
export { BASE_URL, ADMIN_USER };
+31
View File
@@ -0,0 +1,31 @@
import { test } from '@playwright/test';
export type AutomationStatus = 'automated' | 'partial' | 'manual';
/**
* Marks a test as MANUAL — annotates it and skips execution in automated runs.
* The test body should document the required manual steps as comments.
*
* In the HTML test plan report this test appears as 🔴 MANUAL.
*/
export function markManual(reason: string): void {
test.info().annotations.push({ type: 'automationStatus', description: 'manual' });
test.info().annotations.push({ type: 'manualReason', description: reason });
test.skip(true, `⚠️ MANUAL TEST — ${reason}`);
}
/**
* Marks a test as PARTIALLY automatable — automated steps run normally;
* manual steps are annotated but skipped.
*
* In the HTML test plan report this test appears as ⚡ PARTIAL.
*/
export function markPartial(notes: string): void {
test.info().annotations.push({ type: 'automationStatus', description: 'partial' });
test.info().annotations.push({ type: 'partialNotes', description: notes });
}
/** Tags a test as "write" type — the result must be attached to the report. */
export function markWrite(): void {
test.info().annotations.push({ type: 'testType', description: 'write' });
}
+36
View File
@@ -0,0 +1,36 @@
import { TestInfo } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const RESULTS_DIR = path.join(process.cwd(), 'test-results', 'write-outputs');
/**
* Attaches captured data to the test report and writes it to disk.
* Used by all "write" type tests to persist their recorded output.
*/
export async function attachWriteResult(
testInfo: TestInfo,
data: Record<string, unknown>,
label = 'recorded-output',
): Promise<void> {
if (!fs.existsSync(RESULTS_DIR)) {
fs.mkdirSync(RESULTS_DIR, { recursive: true });
}
const sanitizedTitle = testInfo.title.replace(/[^a-z0-9-_]/gi, '_').slice(0, 100);
const filePath = path.join(RESULTS_DIR, `${sanitizedTitle}.json`);
const output = {
testId: testInfo.title,
capturedAt: new Date().toISOString(),
status: 'recorded',
data,
};
fs.writeFileSync(filePath, JSON.stringify(output, null, 2), 'utf-8');
await testInfo.attach(label, {
body: JSON.stringify(output, null, 2),
contentType: 'application/json',
});
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "pbx-autotest",
"version": "1.0.0",
"description": "QA Automation Test Suite — PBX & Core systems (Playwright + MCP)",
"scripts": {
"test": "playwright test",
"test:core": "playwright test tests/core/",
"test:pbx": "playwright test tests/pbx/",
"test:automated": "playwright test --grep-invert @manual",
"test:partial": "playwright test --grep @partial",
"test:report": "playwright show-report",
"lint": "tsc --noEmit"
},
"devDependencies": {
"@playwright/test": "^1.47.0",
"@types/node": "^22.0.0",
"dotenv": "^16.4.0",
"typescript": "^5.5.0"
}
}
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
export default defineConfig({
testDir: './tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['list'],
],
use: {
baseURL: process.env.BASE_URL ?? 'https://pbx.local:4443',
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
timeout: 90_000,
expect: { timeout: 10_000 },
outputDir: 'test-results',
});
+963
View File
@@ -0,0 +1,963 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QA Automation Test Plan Report</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-automated: #22c55e;
--color-partial: #f59e0b;
--color-manual: #ef4444;
--color-core: #3b82f6;
--color-pbx: #8b5cf6;
--color-check: #64748b;
--color-write: #0ea5e9;
--bg-header: #0f172a;
--bg-subheader: #1e293b;
--bg-body: #f8fafc;
--bg-card: #ffffff;
--bg-row-alt: #f1f5f9;
--border: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg-body);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
}
/* ── HEADER ── */
.site-header {
background: var(--bg-header);
padding: 32px 48px 28px;
border-bottom: 3px solid #334155;
}
.site-header h1 {
font-size: 26px;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.4px;
}
.site-header .subtitle {
font-size: 14px;
color: #94a3b8;
margin-top: 4px;
}
.site-header .meta {
display: flex;
align-items: center;
gap: 20px;
margin-top: 14px;
flex-wrap: wrap;
}
.meta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #cbd5e1;
background: #1e293b;
border: 1px solid #334155;
border-radius: 20px;
padding: 3px 12px;
}
.meta-chip .dot {
width: 7px; height: 7px; border-radius: 50%;
background: #22c55e;
}
/* ── MAIN ── */
main {
max-width: 1500px;
margin: 0 auto;
padding: 32px 32px 60px;
}
/* ── SECTION TITLES ── */
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: .7px;
margin-bottom: 14px;
}
/* ── SUMMARY CARDS ── */
.cards-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 32px;
}
@media (max-width: 900px) { .cards-row { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 500px) { .cards-row { grid-template-columns: 1fr; } }
.card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: 22px 24px 18px;
border-top: 4px solid var(--card-accent, #94a3b8);
position: relative;
overflow: hidden;
}
.card::after {
content: '';
position: absolute;
top: -30px; right: -30px;
width: 100px; height: 100px;
border-radius: 50%;
background: var(--card-accent, #94a3b8);
opacity: .05;
}
.card-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.card-number {
font-size: 42px;
font-weight: 800;
line-height: 1;
color: var(--card-accent, var(--text-primary));
}
.card-pct {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
font-weight: 500;
}
.card-bar-track {
height: 6px;
background: #e2e8f0;
border-radius: 3px;
margin-top: 14px;
overflow: hidden;
}
.card-bar-fill {
height: 100%;
border-radius: 3px;
background: var(--card-accent, #94a3b8);
transition: width .6s ease;
}
/* ── CHARTS ROW ── */
.charts-row {
display: grid;
grid-template-columns: 380px 1fr;
gap: 20px;
margin-bottom: 32px;
}
@media (max-width: 900px) { .charts-row { grid-template-columns: 1fr; } }
.chart-card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: 24px;
}
.chart-card h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: .6px;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.chart-wrap {
position: relative;
}
.chart-wrap-doughnut {
max-width: 280px;
margin: 0 auto;
}
.doughnut-legend {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 18px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.legend-dot {
width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
}
.legend-val { margin-left: auto; font-weight: 600; color: var(--text-primary); }
/* ── FILTER BAR ── */
.filter-bar {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 16px 20px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.filter-bar label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
margin-right: 4px;
}
.filter-group {
display: flex;
align-items: center;
gap: 6px;
}
.filter-bar select, .filter-bar input {
font-size: 13px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-body);
color: var(--text-primary);
outline: none;
transition: border-color .15s;
}
.filter-bar select:focus, .filter-bar input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
}
.filter-bar input[type="text"] { min-width: 220px; }
.filter-divider { width: 1px; height: 24px; background: var(--border); }
.btn-reset {
font-size: 12px;
font-weight: 600;
padding: 6px 14px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg-body);
color: var(--text-secondary);
cursor: pointer;
transition: background .15s, color .15s;
}
.btn-reset:hover { background: #e2e8f0; color: var(--text-primary); }
.result-count {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
}
.result-count span { color: var(--text-primary); font-size: 14px; }
/* ── TABLE ── */
.table-wrap {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.table-scroll { overflow-x: auto; }
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead th {
background: var(--bg-subheader);
color: #94a3b8;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .7px;
padding: 12px 14px;
text-align: left;
white-space: nowrap;
border-bottom: 2px solid #334155;
position: sticky;
top: 0;
z-index: 1;
cursor: pointer;
user-select: none;
transition: color .15s;
}
thead th:hover { color: #e2e8f0; }
thead th.sorted-asc::after { content: ' ↑'; color: #3b82f6; }
thead th.sorted-desc::after { content: ' ↓'; color: #3b82f6; }
tbody tr {
border-left: 3px solid transparent;
transition: background .1s;
}
tbody tr:nth-child(even) { background: var(--bg-row-alt); }
tbody tr:hover { background: #e0f2fe !important; }
tbody tr.status-automated { border-left-color: var(--color-automated); }
tbody tr.status-partial { border-left-color: var(--color-partial); }
tbody tr.status-manual { border-left-color: var(--color-manual); }
td {
padding: 10px 14px;
color: var(--text-primary);
vertical-align: middle;
border-bottom: 1px solid var(--border);
}
td.num {
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
width: 42px;
text-align: center;
}
td.td-id {
font-family: "SF Mono", "Consolas", "Liberation Mono", monospace;
font-size: 11.5px;
color: #334155;
max-width: 200px;
}
.id-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 190px;
}
td.td-desc {
min-width: 260px;
max-width: 380px;
color: var(--text-primary);
}
td.td-file {
font-family: "SF Mono", "Consolas", monospace;
font-size: 11px;
color: var(--text-muted);
max-width: 200px;
}
.file-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── BADGES ── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .4px;
padding: 2px 9px;
border-radius: 20px;
white-space: nowrap;
}
.badge-automated { background: #dcfce7; color: #15803d; }
.badge-partial { background: #fef3c7; color: #b45309; }
.badge-manual { background: #fee2e2; color: #b91c1c; }
.badge-core { background: #dbeafe; color: #1d4ed8; }
.badge-pbx { background: #ede9fe; color: #6d28d9; }
.badge-check { background: #f1f5f9; color: #475569; }
.badge-write { background: #e0f2fe; color: #0369a1; }
.badge-dot {
width: 6px; height: 6px; border-radius: 50%;
}
.badge-automated .badge-dot { background: #15803d; }
.badge-partial .badge-dot { background: #b45309; }
.badge-manual .badge-dot { background: #b91c1c; }
/* ── COPY BUTTON ── */
.copy-btn {
display: none;
font-size: 10px;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
border: 1px solid var(--border);
background: #fff;
color: var(--text-secondary);
cursor: pointer;
margin-left: 4px;
transition: background .1s, color .1s;
vertical-align: middle;
}
.copy-btn:hover { background: #3b82f6; color: #fff; border-color: #3b82f6; }
tbody tr:hover .copy-btn { display: inline; }
.copy-btn.copied { background: #22c55e; color: #fff; border-color: #22c55e; }
/* ── NO RESULTS ── */
.no-results {
padding: 48px;
text-align: center;
color: var(--text-muted);
font-size: 14px;
display: none;
}
.no-results svg { display: block; margin: 0 auto 12px; opacity: .3; }
/* ── FOOTER ── */
footer {
text-align: center;
padding: 24px 32px;
font-size: 12px;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 40px;
}
footer strong { color: var(--text-secondary); }
</style>
</head>
<body>
<!-- ══════════════════════════════════ HEADER ══════════════════════════════════ -->
<header class="site-header">
<h1>QA Automation Test Plan Report</h1>
<p class="subtitle">PBX / Core Systems</p>
<div class="meta">
<span class="meta-chip"><span class="dot"></span> Generated 2026-05-22</span>
<span class="meta-chip">Playwright + TypeScript + MCP</span>
<span class="meta-chip">86 Test Cases</span>
</div>
</header>
<!-- ══════════════════════════════════ MAIN ══════════════════════════════════ -->
<main>
<!-- SUMMARY CARDS -->
<p class="section-title">Overview</p>
<div class="cards-row">
<div class="card" style="--card-accent:#3b82f6;">
<div class="card-label">Total Tests</div>
<div class="card-number">86</div>
<div class="card-pct">Across 18 groups</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:100%;"></div></div>
</div>
<div class="card" style="--card-accent:#22c55e;">
<div class="card-label">Automated</div>
<div class="card-number">34</div>
<div class="card-pct">39.5% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:39.5%;"></div></div>
</div>
<div class="card" style="--card-accent:#f59e0b;">
<div class="card-label">Partial</div>
<div class="card-number">16</div>
<div class="card-pct">18.6% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:18.6%;"></div></div>
</div>
<div class="card" style="--card-accent:#ef4444;">
<div class="card-label">Manual</div>
<div class="card-number">36</div>
<div class="card-pct">41.9% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:41.9%;"></div></div>
</div>
</div>
<!-- CHARTS -->
<p class="section-title">Charts</p>
<div class="charts-row">
<!-- Doughnut -->
<div class="chart-card">
<h3>Automation Distribution</h3>
<div class="chart-wrap chart-wrap-doughnut">
<canvas id="chartDoughnut"></canvas>
</div>
<div class="doughnut-legend">
<div class="legend-item"><span class="legend-dot" style="background:#22c55e;"></span> Automated <span class="legend-val">34</span></div>
<div class="legend-item"><span class="legend-dot" style="background:#f59e0b;"></span> Partial <span class="legend-val">16</span></div>
<div class="legend-item"><span class="legend-dot" style="background:#ef4444;"></span> Manual <span class="legend-val">36</span></div>
</div>
</div>
<!-- Horizontal stacked bar -->
<div class="chart-card">
<h3>Automation by Group</h3>
<div class="chart-wrap" style="height:520px; position:relative;">
<canvas id="chartBar"></canvas>
</div>
</div>
</div>
<!-- FILTER BAR -->
<div class="filter-bar">
<div class="filter-group">
<label for="fModule">Module</label>
<select id="fModule">
<option value="">All</option>
<option value="core">Core</option>
<option value="pbx">PBX</option>
</select>
</div>
<div class="filter-group">
<label for="fGroup">Group</label>
<select id="fGroup">
<option value="">All</option>
<option value="core-repository">core-repository</option>
<option value="core-documentation">core-documentation</option>
<option value="core-features">core-features</option>
<option value="core-virtualization">core-virtualization</option>
<option value="core-security">core-security</option>
<option value="core-installation">core-installation</option>
<option value="core-upgrade">core-upgrade</option>
<option value="core-interoperability">core-interoperability</option>
<option value="pbx-configuration">pbx-configuration</option>
<option value="pbx-extensions">pbx-extensions</option>
<option value="pbx-calls">pbx-calls</option>
<option value="pbx-codecs">pbx-codecs</option>
<option value="pbx-calls-security">pbx-calls-security</option>
<option value="pbx-performance">pbx-performance</option>
<option value="pbx-availability">pbx-availability</option>
<option value="pbx-management">pbx-management</option>
<option value="pbx-logs">pbx-logs</option>
<option value="pbx-regression">pbx-regression</option>
</select>
</div>
<div class="filter-group">
<label for="fAction">Action</label>
<select id="fAction">
<option value="">All</option>
<option value="check">check</option>
<option value="write">write</option>
</select>
</div>
<div class="filter-group">
<label for="fStatus">Status</label>
<select id="fStatus">
<option value="">All</option>
<option value="automated">automated</option>
<option value="partial">partial</option>
<option value="manual">manual</option>
</select>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<label for="fSearch">Search</label>
<input type="text" id="fSearch" placeholder="ID, description, group, file…" />
</div>
<button class="btn-reset" id="btnReset">Clear</button>
<div class="result-count" id="resultCount"><span id="visibleCount">86</span> / 86 tests</div>
</div>
<!-- TABLE -->
<div class="table-wrap">
<div class="table-scroll">
<table id="testTable">
<thead>
<tr>
<th data-col="idx">#</th>
<th data-col="id">Test ID</th>
<th data-col="group">Group</th>
<th data-col="module">Module</th>
<th data-col="action">Action</th>
<th data-col="description">Description</th>
<th data-col="status">Status</th>
<th data-col="file">Spec File</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="no-results" id="noResults">
<svg width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
No tests match the current filters.
</div>
</div>
</div>
</main>
<footer>
Generated automatically by <strong>QA Automation Framework</strong> &middot; Playwright + TypeScript + MCP
</footer>
<script>
/* ══════════════════════════════ DATA ══════════════════════════════ */
const TESTS = [
// === CORE-REPOSITORY (6) ===
{ id: "core-repository-check-presence_of_release_notes_and_changelog", group: "core-repository", module: "core", action: "check", description: "Verify Release Notes and Changelog are present in the software repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_trouble_shooting_guide", group: "core-repository", module: "core", action: "check", description: "Verify Troubleshooting Guide is present in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_all_mandatory_core-documentation_files", group: "core-repository", module: "core", action: "check", description: "Verify all mandatory core documentation files are listed in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_main_software_version_all_hypervisors", group: "core-repository", module: "core", action: "check", description: "Verify main software version packages are available for all supported hypervisors", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_patch_version_and_its_compatibility", group: "core-repository", module: "core", action: "check", description: "Verify patch version packages and compatibility matrices are present", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_companion_tools_and_supporting_scripts", group: "core-repository", module: "core", action: "check", description: "Verify companion tools and supporting scripts are available in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
// === CORE-DOCUMENTATION (4) ===
{ id: "core-documentation-write-release_notes_feature_changes_and_known_issues", group: "core-documentation", module: "core", action: "write", description: "Extract and record Release Notes, feature changes, and known issues", status: "automated", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-check-accuracy_installation_and_configuration_guide", group: "core-documentation", module: "core", action: "check", description: "Verify accuracy of the Installation and Configuration Guide against actual system behaviour", status: "manual", manualReason: "Requires expert human review: QA engineer must follow the guide step-by-step on a clean system", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-check-accuracy_trouble_shooting_guide", group: "core-documentation", module: "core", action: "check", description: "Verify the Troubleshooting Guide procedures are accurate and complete", status: "manual", manualReason: "Requires expert human review to validate each troubleshooting scenario on a real system", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-write-changed_cli_configuration_commands", group: "core-documentation", module: "core", action: "write", description: "Extract and record changed CLI configuration commands for this release", status: "automated", file: "tests/core/documentation.spec.ts" },
// === CORE-FEATURES (7) ===
{ id: "core-features-write-summary_of_each_new_feature_per_release", group: "core-features", module: "core", action: "write", description: "Extract and record a summary of each new feature included in this release", status: "automated", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-integration_of_new_features_with_existing_call_processing_and_modules", group: "core-features", module: "core", action: "check", description: "Verify new features integrate correctly with existing call processing and system modules", status: "partial", partialNotes: "UI config checks automated; actual call-flow integration requires active telephony endpoints", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-backward_compatibility_of_new_features", group: "core-features", module: "core", action: "check", description: "Verify new features are backward-compatible with existing configurations and older endpoint firmware", status: "partial", partialNotes: "Configuration import checks automated; functional compatibility with older endpoints requires manual testing", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-performance_and_resource_impact_assessment_of_new_features", group: "core-features", module: "core", action: "check", description: "Assess CPU/memory/network resource impact of new features under representative call load", status: "manual", manualReason: "Requires SIPp load generator, performance monitoring infrastructure, and sustained load testing", file: "tests/core/features.spec.ts" },
{ id: "core-features-write-analysis_of_new_security_risks", group: "core-features", module: "core", action: "write", description: "Identify, analyse, and document new security risks introduced by features in this release", status: "manual", manualReason: "Security risk analysis requires expert threat modelling, source code review, and human judgment", file: "tests/core/features.spec.ts" },
{ id: "core-features-write-ui_ux_and_admin_portal_changes_validation", group: "core-features", module: "core", action: "write", description: "Capture and record all UI/UX changes in the admin portal for this release", status: "automated", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-provisioning_changes_for_new_features", group: "core-features", module: "core", action: "check", description: "Verify provisioning settings for new features are accessible in the admin portal", status: "automated", file: "tests/core/features.spec.ts" },
// === CORE-VIRTUALIZATION (8) ===
{ id: "core-virtualization-write-supported_hypervisors_deployment", group: "core-virtualization", module: "core", action: "write", description: "Record the list of supported hypervisors and deployment methods from the download portal", status: "automated", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-guest_agent_integration", group: "core-virtualization", module: "core", action: "write", description: "Record guest agent integration status for each supported hypervisor", status: "manual", manualReason: "Requires direct access to hypervisor console (vCenter, Hyper-V Manager, virsh)", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-resource_allocation_vcpu_vram_vdisk", group: "core-virtualization", module: "core", action: "write", description: "Record vCPU, vRAM, and vDisk resource allocation for deployment templates", status: "manual", manualReason: "Hypervisor management plane access required; not exposed in PBX admin portal", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-virtual_networking_configuration", group: "core-virtualization", module: "core", action: "write", description: "Record virtual networking configuration: virtual switches, VLANs, NIC assignments", status: "manual", manualReason: "Requires access to hypervisor networking configuration and virtual switch management", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-live_migration_and_active_call_preservation", group: "core-virtualization", module: "core", action: "check", description: "Verify VM live migration preserves active calls without interruption", status: "manual", manualReason: "Requires HA infrastructure with 2+ hypervisor hosts and active SIP call sessions during migration", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-snapshot_backup_restore_and_consistency", group: "core-virtualization", module: "core", action: "check", description: "Verify VM snapshot and restore operations maintain full system consistency", status: "partial", partialNotes: "Post-restore consistency checks via admin portal automated; snapshot/restore operation needs hypervisor access", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-hypervisor_level_high_availability", group: "core-virtualization", module: "core", action: "check", description: "Verify hypervisor-level HA restarts the PBX VM after a host failure", status: "manual", manualReason: "Requires physical HA cluster with multiple hypervisor hosts; host failure must be simulated", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-storage_thin_vs_thick_provisioning", group: "core-virtualization", module: "core", action: "write", description: "Record storage provisioning type and performance implications for each hypervisor", status: "manual", manualReason: "Storage details only visible in hypervisor storage management interface", file: "tests/core/virtualization.spec.ts" },
// === CORE-SECURITY (11) ===
{ id: "core-security-write-strong_password_policy", group: "core-security", module: "core", action: "write", description: "Navigate to security settings and record the current password policy configuration", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-sip_tls_and_srtp_encryption_enabled_by_default", group: "core-security", module: "core", action: "check", description: "Verify SIP TLS and SRTP media encryption are enabled by default", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-ip_access_control_lists", group: "core-security", module: "core", action: "check", description: "Verify IP Access Control Lists are properly configured and enforced", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-unnecessary_services_and_ports_are_disabled", group: "core-security", module: "core", action: "check", description: "Verify unnecessary services and network ports are disabled", status: "partial", partialNotes: "Service status via admin portal automated; port scanning requires nmap from external host", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-security_event_logging_and_audit_trail_is_active", group: "core-security", module: "core", action: "check", description: "Verify security event logging and audit trail are active and recording events", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-auto_enrollment_certificate_management_process", group: "core-security", module: "core", action: "check", description: "Verify auto-enrollment certificate management (ACME/Let's Encrypt) is configured and functional", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-known_vulnerable_dependencies_in_release_artifacts", group: "core-security", module: "core", action: "check", description: "Verify release artifacts do not contain known vulnerable dependencies (CVE scan)", status: "partial", partialNotes: "Vulnerability report page in portal automated; actual CVE scanning requires Trivy/OWASP-DC in CI", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-previous_security_findings_have_been_remediated_before_release", group: "core-security", module: "core", action: "check", description: "Verify all previously identified security findings are remediated before release", status: "manual", manualReason: "Requires access to security issue tracker and cross-referencing CVEs with the release changelog", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-obtain_formal_security_team_sign_off_for_the_new_version", group: "core-security", module: "core", action: "check", description: "Obtain formal written sign-off from the Security team approving the release", status: "manual", manualReason: "Governance gate requiring human decision and formal sign-off document; cannot be automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-verify_role_based_access_control_and_least_privilege_principle", group: "core-security", module: "core", action: "check", description: "Verify RBAC roles are correctly defined and least-privilege principle is enforced", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-encryption_of_sensitive_data_at_rest_and_in_transit", group: "core-security", module: "core", action: "check", description: "Verify sensitive data is encrypted at rest (DB) and in transit (HTTPS/TLS)", status: "partial", partialNotes: "HTTPS/TLS configuration verified via portal; DB-level at-rest encryption requires SSH/CLI access", file: "tests/core/security.spec.ts" },
// === CORE-INSTALLATION (4) ===
{ id: "core-installation-check-fresh_installation", group: "core-installation", module: "core", action: "check", description: "Verify complete fresh installation from scratch succeeds on each supported hypervisor", status: "manual", manualReason: "Requires provisioning a clean VM, mounting ISO/OVA, and running the installation wizard", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-post_install_service_status_and_port_verification", group: "core-installation", module: "core", action: "check", description: "Verify all required services are running and expected ports are open after installation", status: "partial", partialNotes: "Service status via admin portal automated; TCP port verification requires nmap from external host", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-initial_configuration", group: "core-installation", module: "core", action: "check", description: "Verify the initial configuration wizard completes successfully", status: "automated", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-license_activation", group: "core-installation", module: "core", action: "check", description: "Verify license activation completes successfully and licensed features are unlocked", status: "automated", file: "tests/core/installation.spec.ts" },
// === CORE-UPGRADE (6) ===
{ id: "core-upgrade-check-in_place_upgrade_from_previous_major_version", group: "core-upgrade", module: "core", action: "check", description: "Verify in-place upgrade from the previous major version completes successfully without data loss", status: "manual", manualReason: "Requires a running instance of the previous version and dedicated upgrade test environment", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-configuration_and_database_migration_validation", group: "core-upgrade", module: "core", action: "check", description: "Verify configuration objects and database content migrate correctly after upgrade", status: "partial", partialNotes: "UI configuration checks automated; DB schema integrity validation requires direct database access", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-schema_upgrade_and_data_integrity_check", group: "core-upgrade", module: "core", action: "check", description: "Verify database schema is correctly upgraded and all data maintains integrity", status: "manual", manualReason: "Requires direct database access (psql/mysql) and schema comparison tooling", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-rollback_procedure_execution_and_verification", group: "core-upgrade", module: "core", action: "check", description: "Verify the rollback procedure successfully reverts the system to the previous version", status: "manual", manualReason: "Requires VM snapshot revert or package downgrade; needs physical/hypervisor-level access", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-post_upgrade_regression_of_core_features", group: "core-upgrade", module: "core", action: "check", description: "Run automated regression checks on core portal features after upgrade", status: "automated", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-zero_downtime_upgrade_where_supported", group: "core-upgrade", module: "core", action: "check", description: "Verify zero-downtime upgrade keeps the service available throughout the upgrade process", status: "manual", manualReason: "Requires active call sessions monitored during the upgrade window; calls cannot be managed by Playwright", file: "tests/core/upgrade.spec.ts" },
// === CORE-INTEROPERABILITY (2) ===
{ id: "core-interoperability-check-addm", group: "core-interoperability", module: "core", action: "check", description: "Verify ADDM integration is configured and reporting discovery data correctly", status: "automated", file: "tests/core/interoperability.spec.ts" },
{ id: "core-interoperability-check-siem", group: "core-interoperability", module: "core", action: "check", description: "Verify SIEM integration is configured and forwarding security events", status: "automated", file: "tests/core/interoperability.spec.ts" },
// === PBX-CONFIGURATION (4) ===
{ id: "pbx-configuration-check-initial_system_setup_and_network_settings", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify initial PBX system setup and network settings are correctly configured", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-dialplan_route_and_outbound_rule_creation", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify creation and validation of dial plan rules and outbound routes", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-backup_restore_full_system_configuration", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify full system configuration backup and restore operations work correctly", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-bulk_csv_import_export_of_settings", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify bulk CSV import and export of settings", status: "automated", file: "tests/pbx/configuration.spec.ts" },
// === PBX-EXTENSIONS (3) ===
{ id: "pbx-extensions-check-create_modify_delete_sip_extensions", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify full CRUD lifecycle for SIP extensions in the admin portal", status: "automated", file: "tests/pbx/extensions.spec.ts" },
{ id: "pbx-extensions-check-extension_registration_with_deskphones_softphones", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify SIP extension registers successfully from a desk phone and a softphone client", status: "manual", manualReason: "Requires physical SIP desk phone (Yealink, Polycom) or softphone (Zoiper, MicroSIP)", file: "tests/pbx/extensions.spec.ts" },
{ id: "pbx-extensions-check-device_provisioning", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify automatic device provisioning: profile generation and server URL accessible", status: "partial", partialNotes: "Provisioning profile creation via admin portal automated; actual device provisioning requires physical phone", file: "tests/pbx/extensions.spec.ts" },
// === PBX-CALLS (7) ===
{ id: "pbx-calls-check-internal_extension_to_extension_audio_call", group: "pbx-calls", module: "pbx", action: "check", description: "Verify internal extension-to-extension audio call connects with bi-directional audio", status: "manual", manualReason: "Requires two active SIP endpoints and human verification of two-way audio quality", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-inbound_call_from_sip_trunk_pstn", group: "pbx-calls", module: "pbx", action: "check", description: "Verify inbound call from a SIP trunk/PSTN routes correctly to the configured destination", status: "manual", manualReason: "Requires live SIP trunk connectivity to PSTN and a real DID number", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-outbound_call_to_sip_trunk_pstn_with_caller_id", group: "pbx-calls", module: "pbx", action: "check", description: "Verify outbound call via SIP trunk presents the correct Caller ID on the receiving end", status: "manual", manualReason: "Requires live SIP trunk and verification of Caller ID at receiving PSTN party", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-caller_id_presentation_restriction_and_pai", group: "pbx-calls", module: "pbx", action: "check", description: "Verify Caller ID presentation, Anonymous restriction (CLIR), and P-Asserted-Identity header", status: "manual", manualReason: "Requires active call sessions and SIP packet capture to verify PAI header values", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-call_hold_resume_and_music_on_hold", group: "pbx-calls", module: "pbx", action: "check", description: "Verify call hold, resume, and Music on Hold playback work correctly", status: "manual", manualReason: "Requires an active two-party call; MoH requires audio monitoring with real SIP phone hardware", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-call_waiting_notification_and_switching", group: "pbx-calls", module: "pbx", action: "check", description: "Verify call waiting notification and switching between active and waiting calls", status: "manual", manualReason: "Requires concurrent call sessions on the same extension with physical/softphone capable of call-waiting", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-three_way_conference_and_multi_party_bridge", group: "pbx-calls", module: "pbx", action: "check", description: "Verify three-way conference calling and multi-party conference bridge functionality", status: "manual", manualReason: "Requires three or more active SIP endpoints and audio verification by human listeners", file: "tests/pbx/calls.spec.ts" },
// === PBX-CODECS (3) ===
{ id: "pbx-codecs-check-codec_negotiation_g711_g729_opus_g722", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify successful codec negotiation for G.711, G.729, Opus, and G.722", status: "manual", manualReason: "Requires SIP call traffic with codec-constrained endpoints and Wireshark packet capture to verify SDP negotiation", file: "tests/pbx/codecs.spec.ts" },
{ id: "pbx-codecs-check-dtmf_transmission_rfc2833_inband_sip_info", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify DTMF digit transmission via RFC2833, in-band audio, and SIP INFO methods", status: "manual", manualReason: "Requires active call sessions with DTMF generation, packet capture, and IVR system for verification", file: "tests/pbx/codecs.spec.ts" },
{ id: "pbx-codecs-check-t38_fax_passthrough_and_error_correction", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify T.38 fax passthrough and error correction (ECM) work correctly", status: "manual", manualReason: "Requires fax hardware or fax emulator (HylaFAX) and T.38 stack support", file: "tests/pbx/codecs.spec.ts" },
// === PBX-CALLS-SECURITY (3) ===
{ id: "pbx-calls-security-check-srtp_sdes_or_dtls_media_encryption", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify SRTP with SDES or DTLS-SRTP is enabled for media encryption on calls", status: "partial", partialNotes: "Encryption config verified via admin portal; confirming live RTP is encrypted requires Wireshark during active call", file: "tests/pbx/calls-security.spec.ts" },
{ id: "pbx-calls-security-check-acl_ip_whitelisting_and_rate_limiting", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify ACL IP whitelisting rules and SIP rate limiting are configured and enforced", status: "partial", partialNotes: "ACL/rate-limit config via portal automated; functional enforcement testing requires network-level testing from non-whitelisted IP", file: "tests/pbx/calls-security.spec.ts" },
{ id: "pbx-calls-security-check-toll_fraud_prevention_rules_and_alerting", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify toll fraud prevention rules and alert notifications are configured", status: "automated", file: "tests/pbx/calls-security.spec.ts" },
// === PBX-PERFORMANCE (3) ===
{ id: "pbx-performance-check-cpu_memory_disk_io_utilization_under_sustained_load", group: "pbx-performance", module: "pbx", action: "check", description: "Verify CPU, memory, and disk I/O remain within limits under sustained call load", status: "manual", manualReason: "Requires SIPp load generator, server-side performance monitoring, and dedicated test environment", file: "tests/pbx/performance.spec.ts" },
{ id: "pbx-performance-check-long_duration_stability_72h_test", group: "pbx-performance", module: "pbx", action: "check", description: "Verify system stability under continuous call load over a 72-hour period", status: "manual", manualReason: "72-hour test requires dedicated environment, SIPp continuous load, and automated monitoring with alerting", file: "tests/pbx/performance.spec.ts" },
{ id: "pbx-performance-check-memory_leak_and_resource_cleanup_detection", group: "pbx-performance", module: "pbx", action: "check", description: "Detect memory leaks and verify proper resource cleanup over extended operation", status: "manual", manualReason: "Requires extended monitoring with active call load and memory profiling tools (Valgrind, heap dumps)", file: "tests/pbx/performance.spec.ts" },
// === PBX-AVAILABILITY (4) ===
{ id: "pbx-availability-check-active_passive_or_active_active_failover", group: "pbx-availability", module: "pbx", action: "check", description: "Verify Active-Passive or Active-Active failover transitions correctly on node failure", status: "manual", manualReason: "Requires HA cluster with 2+ nodes; failover must be triggered by stopping the active node", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-database_replication_and_sync_integrity", group: "pbx-availability", module: "pbx", action: "check", description: "Verify database replication between HA nodes maintains consistency and acceptable lag", status: "manual", manualReason: "Requires HA database cluster; verifying replication lag needs direct database access", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-automatic_failover_and_call_preservation", group: "pbx-availability", module: "pbx", action: "check", description: "Verify automatic failover preserves active calls (no drops, no audio interruption > 200ms)", status: "manual", manualReason: "Requires active call sessions during failover and simultaneous call-state monitoring", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-geographic_redundancy_and_geo_distribution", group: "pbx-availability", module: "pbx", action: "check", description: "Verify geo-distributed deployment handles site failover without service interruption", status: "manual", manualReason: "Requires multi-site infrastructure across geographic locations or WAN simulation", file: "tests/pbx/availability.spec.ts" },
// === PBX-MANAGEMENT (2) ===
{ id: "pbx-management-check-cli_management", group: "pbx-management", module: "pbx", action: "check", description: "Verify CLI management interface is accessible and key management commands execute correctly", status: "partial", partialNotes: "Web console (if present) verified via Playwright; full CLI coverage requires SSH automation integrated in CI pipeline", file: "tests/pbx/management.spec.ts" },
{ id: "pbx-management-check-gui_management", group: "pbx-management", module: "pbx", action: "check", description: "Verify all main admin portal sections load correctly and management actions are functional", status: "automated", file: "tests/pbx/management.spec.ts" },
// === PBX-LOGS (5) ===
{ id: "pbx-logs-check-cdr_generation_accuracy_completeness_and_export", group: "pbx-logs", module: "pbx", action: "check", description: "Verify CDR records contain all required fields and can be exported to CSV/PDF", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-real_time_active_call_and_registration_monitoring", group: "pbx-logs", module: "pbx", action: "check", description: "Verify real-time monitoring dashboard shows active calls and SIP registrations", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-detailed_logging_levels_rotation_and_search", group: "pbx-logs", module: "pbx", action: "check", description: "Verify logging levels, log rotation, and log search/filter functionality", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-threshold_alarms_cpu_channels_disk_memory", group: "pbx-logs", module: "pbx", action: "check", description: "Verify threshold-based alarms for CPU, active channels, disk, and memory usage", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-call_quality_metrics_mos_jitter_loss_per_call", group: "pbx-logs", module: "pbx", action: "check", description: "Verify per-call quality metrics (MOS score, jitter, packet loss) are collected and displayed", status: "partial", partialNotes: "QoS metrics display UI automated; actual metric generation requires active call sessions with network impairment", file: "tests/pbx/logs.spec.ts" },
// === PBX-REGRESSION (4) ===
{ id: "pbx-regression-check-core_call_features_after_any_code_change", group: "pbx-regression", module: "pbx", action: "check", description: "Run regression checks on core features after any code change to detect regressions", status: "partial", partialNotes: "Admin portal regression automated; call-flow regression (inbound/outbound/hold/transfer) requires SIP endpoints", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-previously_fixed_bugs_and_edge_cases_reverification", group: "pbx-regression", module: "pbx", action: "check", description: "Verify all previously fixed bugs and known edge cases have not regressed", status: "partial", partialNotes: "UI bug regressions automated; telephony-layer bug regressions require SIP client or SSH verification", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-performance_baselines_comparison_against_previous_version", group: "pbx-regression", module: "pbx", action: "check", description: "Compare key performance metrics against baselines from the previous version", status: "manual", manualReason: "Requires SIPp load tests and statistical comparison of results against previous version baseline", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-security_hardening_and_vulnerability_scan_baseline", group: "pbx-regression", module: "pbx", action: "check", description: "Verify security hardening settings and compare vulnerability scan baseline against previous version", status: "partial", partialNotes: "Security config checks via portal automated; full CVE scan baseline requires Trivy/OpenVAS in CI pipeline", file: "tests/pbx/regression.spec.ts" },
];
/* ══════════════════════════════ STATE ══════════════════════════════ */
let sortCol = null;
let sortDir = 1; // 1 = asc, -1 = desc
/* ══════════════════════════════ CHARTS ══════════════════════════════ */
// Doughnut
new Chart(document.getElementById('chartDoughnut'), {
type: 'doughnut',
data: {
labels: ['Automated', 'Partial', 'Manual'],
datasets: [{
data: [34, 16, 36],
backgroundColor: ['#22c55e', '#f59e0b', '#ef4444'],
borderColor: ['#16a34a', '#d97706', '#dc2626'],
borderWidth: 2,
hoverOffset: 6
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => {
const val = ctx.raw;
const pct = ((val / 86) * 100).toFixed(1);
return ` ${ctx.label}: ${val} (${pct}%)`;
}
}
}
},
cutout: '68%'
}
});
// Compute group counts
const GROUP_ORDER = [
'core-repository','core-documentation','core-features','core-virtualization',
'core-security','core-installation','core-upgrade','core-interoperability',
'pbx-configuration','pbx-extensions','pbx-calls','pbx-codecs',
'pbx-calls-security','pbx-performance','pbx-availability','pbx-management',
'pbx-logs','pbx-regression'
];
const groupCounts = {};
GROUP_ORDER.forEach(g => { groupCounts[g] = { automated: 0, partial: 0, manual: 0 }; });
TESTS.forEach(t => { if (groupCounts[t.group]) groupCounts[t.group][t.status]++; });
const barLabels = GROUP_ORDER.map(g => g);
const barAuto = GROUP_ORDER.map(g => groupCounts[g].automated);
const barPartial = GROUP_ORDER.map(g => groupCounts[g].partial);
const barManual = GROUP_ORDER.map(g => groupCounts[g].manual);
new Chart(document.getElementById('chartBar'), {
type: 'bar',
data: {
labels: barLabels,
datasets: [
{ label: 'Automated', data: barAuto, backgroundColor: '#22c55e', borderRadius: 2 },
{ label: 'Partial', data: barPartial, backgroundColor: '#f59e0b', borderRadius: 2 },
{ label: 'Manual', data: barManual, backgroundColor: '#ef4444', borderRadius: 2 }
]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: {
font: { size: 11 },
color: '#475569',
boxWidth: 12,
padding: 16
}
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
stacked: true,
grid: { color: '#f1f5f9' },
ticks: {
font: { size: 11 },
color: '#94a3b8',
stepSize: 1
}
},
y: {
stacked: true,
grid: { display: false },
ticks: {
font: { size: 11, family: 'monospace' },
color: '#475569'
}
}
}
}
});
/* ══════════════════════════════ TABLE RENDERING ══════════════════════════════ */
function escHtml(s) {
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
function renderRows(list) {
const tbody = document.getElementById('tableBody');
const noRes = document.getElementById('noResults');
const countEl = document.getElementById('visibleCount');
if (list.length === 0) {
tbody.innerHTML = '';
noRes.style.display = 'block';
countEl.textContent = '0';
return;
}
noRes.style.display = 'none';
countEl.textContent = list.length;
const rows = list.map((t, i) => {
const statusBadge = `<span class="badge badge-${t.status}"><span class="badge-dot"></span>${t.status}</span>`;
const moduleBadge = `<span class="badge badge-${t.module}">${t.module.toUpperCase()}</span>`;
const actionBadge = `<span class="badge badge-${t.action}">${t.action}</span>`;
const shortId = t.id.length > 40 ? t.id.slice(0, 38) + '…' : t.id;
return `<tr class="status-${t.status}" data-id="${escHtml(t.id)}">
<td class="num">${i + 1}</td>
<td class="td-id">
<span class="id-text" title="${escHtml(t.id)}">${escHtml(shortId)}</span>
<button class="copy-btn" onclick="copyId(this,'${escHtml(t.id)}')" title="Copy test ID">Copy</button>
</td>
<td>${escHtml(t.group)}</td>
<td>${moduleBadge}</td>
<td>${actionBadge}</td>
<td class="td-desc">${escHtml(t.description)}</td>
<td>${statusBadge}</td>
<td class="td-file"><span class="file-text" title="${escHtml(t.file)}">${escHtml(t.file)}</span></td>
</tr>`;
});
tbody.innerHTML = rows.join('');
}
function getFiltered() {
const mod = document.getElementById('fModule').value;
const grp = document.getElementById('fGroup').value;
const act = document.getElementById('fAction').value;
const stat = document.getElementById('fStatus').value;
const search = document.getElementById('fSearch').value.trim().toLowerCase();
let list = TESTS.filter(t => {
if (mod && t.module !== mod) return false;
if (grp && t.group !== grp) return false;
if (act && t.action !== act) return false;
if (stat && t.status !== stat) return false;
if (search) {
const hay = (t.id + ' ' + t.description + ' ' + t.group + ' ' + t.file).toLowerCase();
if (!hay.includes(search)) return false;
}
return true;
});
if (sortCol) {
list = [...list].sort((a, b) => {
let va = a[sortCol] || '';
let vb = b[sortCol] || '';
if (sortCol === 'idx') { va = TESTS.indexOf(a); vb = TESTS.indexOf(b); }
if (va < vb) return -1 * sortDir;
if (va > vb) return 1 * sortDir;
return 0;
});
}
return list;
}
function refresh() { renderRows(getFiltered()); }
/* ══════════════════════════════ EVENTS ══════════════════════════════ */
['fModule','fGroup','fAction','fStatus','fSearch'].forEach(id => {
document.getElementById(id).addEventListener('input', refresh);
});
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('fModule').value = '';
document.getElementById('fGroup').value = '';
document.getElementById('fAction').value = '';
document.getElementById('fStatus').value = '';
document.getElementById('fSearch').value = '';
sortCol = null;
sortDir = 1;
document.querySelectorAll('thead th').forEach(th => th.classList.remove('sorted-asc','sorted-desc'));
refresh();
});
// Sorting
document.querySelectorAll('thead th').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.col;
if (sortCol === col) {
sortDir *= -1;
} else {
sortCol = col;
sortDir = 1;
}
document.querySelectorAll('thead th').forEach(h => h.classList.remove('sorted-asc','sorted-desc'));
th.classList.add(sortDir === 1 ? 'sorted-asc' : 'sorted-desc');
refresh();
});
});
/* ══════════════════════════════ COPY ID ══════════════════════════════ */
function copyId(btn, id) {
navigator.clipboard.writeText(id).then(() => {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 1800);
}).catch(() => {
// fallback for non-secure contexts
const ta = document.createElement('textarea');
ta.value = id;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 1800);
});
}
/* ══════════════════════════════ INIT ══════════════════════════════ */
refresh();
</script>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
### core-repository
[[core-repository-check-presence_of_release_notes_and_changelog]]
[[core-repository-check-presence_of_trouble_shooting_guide]]
[[core-repository-check-presence_of_all_mandatory_core-documentation_files]]
[[core-repository-check-presence_of_main_software_version_all_hypervisors]]
[[core-repository-check-presence_of_patch_version_and_its_compatibility]]
[[core-repository-check-presence_of_companion_tools_and_supporting_scripts]]
### core-documentation
[[core-documentation-write-release_notes_feature_changes_and_known_issues]]
[[core-documentation-check-accuracy_installation_and_configuration_guide]]
[[core-documentation-check-accuracy_trouble_shooting_guide]]
[[core-documentation-write-changed_cli_configuration_commands]]
### core-features
[[core-features-write-summary_of_each_new_feature_per_release]]
[[core-features-check-integration_of_new_features_with_existing_call_processing_and_modules]]
[[core-features-check-backward_compatibility_of_new_features]]
[[core-features-check-performance_and_resource_impact_assessment_of_new_features ]]
[[core-features-write-analysis_of_new_security_risks]]
[[core-features-write-ui_ux_and_admin_portal_changes_validation ]]
[[core-features-check-provisioning_changes_for_new_features]]
### core-virtualization
[[core-virtualization-write-supported_hypervisors_deployment]]
[[core-virtualization-write-guest_agent_integration]]
[[core-virtualization-write-resource_allocation_vcpu_vram_vdisk]]
[[core-virtualization-write-virtual_networking_configuration]]
[[core-virtualization-check-live_migration_and_active_call_preservation]]
[[core-virtualization-check-snapshot_backup_restore_and_consistency]]
[[core-virtualization-check-hypervisor_level_high_availability]]
[[core-virtualization-write-storage_thin_vs_thick_provisioning]]
### core-security
[[core-security-write-strong_password_policy]]
[[core-security-check-sip_tls_and_srtp_encryption_enabled_by_default]]
[[core-security-check-ip_access_control_lists]]
[[core-security-check-unnecessary_services_and_ports_are_disabled ]]
[[core-security-check-security_event_logging_and_audit_trail_is_active]]
[[core-security-check-auto_enrollment_certificate_management_process ]]
[[core-security-check-known_vulnerable_dependencies_in_release_artifacts ]]
[[core-security-check-previous_security_findings_have_been_remediated_before_release ]]
[[core-security-check-obtain_formal_security_team_sign_off_for_the_new_version ]]
[[core-security-check-verify_role_based_access_control_and_least_privilege_principle ]]
[[core-security-check-encryption_of_sensitive_data_at_rest_and_in_transit]]
### core-installation
[[core-installation-check-fresh_installation]]
[[core-installation-check-post_install_service_status_and_port_verification]]
[[core-installation-check-initial_configuration]]
[[core-installation-check-license_activation]]
### core-upgrade
[[core-upgrade-check-in_place_upgrade_from_previous_major_version]]
[[core-upgrade-check-configuration_and_database_migration_validation]]
[[core-upgrade-check-schema_upgrade_and_data_integrity_check]]
[[core-upgrade-check-rollback_procedure_execution_and_verification]]
[[core-upgrade-check-post_upgrade_regression_of_core_features]]
[[core-upgrade-check-zero_downtime_upgrade_where_supported]]
### core-interoperability
[[core-interoperability-check-addm]]
[[core-interoperability-check-siem]]
+48
View File
@@ -0,0 +1,48 @@
### pbx-configuration
[[pbx-configuration-check-initial_system_setup_and_network_settings]]
[[pbx-configuration-check-dialplan_route_and_outbound_rule_creation]]
[[pbx-configuration-check-backup_restore_full_system_configuration]]
[[pbx-configuration-check-bulk_csv_import_export_of_settings]]
### pbx-extensions
[[pbx-extensions-check-create_modify_delete_sip_extensions]]
[[pbx-extensions-check-extension_registration_with_deskphones_softphones]]
[[pbx-extensions-check-device_provisioning]]
### pbx-calls
[[pbx-calls-check-internal_extension_to_extension_audio_call]]
[[pbx-calls-check-inbound_call_from_sip_trunk_pstn]]
[[pbx-calls-check-outbound_call_to_sip_trunk_pstn_with_caller_id]]
[[pbx-calls-check-caller_id_presentation_restriction_and_pai]]
[[pbx-calls-check-call_hold_resume_and_music_on_hold]]
[[pbx-calls-check-call_waiting_notification_and_switching]]
[[pbx-calls-check-three_way_conference_and_multi_party_bridge]]
### pbx-codecs
[[pbx-codecs-check-codec_negotiation_g711_g729_opus_g722]]
[[pbx-codecs-check-dtmf_transmission_rfc2833_inband_sip_info]]
[[pbx-codecs-check-t38_fax_passthrough_and_error_correction]]
### pbx-calls-security
[[pbx-calls-security-check-srtp_sdes_or_dtls_media_encryption]]
[[pbx-calls-security-check-acl_ip_whitelisting_and_rate_limiting]]
[[pbx-calls-security-check-toll_fraud_prevention_rules_and_alerting]]
### pbx-performance
[[pbx-performance-check-cpu_memory_disk_io_utilization_under_sustained_load]]
[[pbx-performance-check-long_duration_stability_72h_test]]
[[pbx-performance-check-memory_leak_and_resource_cleanup_detection]]
### pbx-availability
[[pbx-availability-check-active_passive_or_active_active_failover]]
[[pbx-availability-check-database_replication_and_sync_integrity]]
[[pbx-availability-check-automatic_failover_and_call_preservation]]
[[pbx-availability-check-geographic_redundancy_and_geo_distribution]]
### pbx-management
[[pbx-management-check-cli_management]]
[[pbx-management-check-gui_management]]
### pbx-logs
[[pbx-logs-check-cdr_generation_accuracy_completeness_and_export]]
[[pbx-logs-check-real_time_active_call_and_registration_monitoring]]
[[pbx-logs-check-detailed_logging_levels_rotation_and_search]]
[[pbx-logs-check-threshold_alarms_cpu_channels_disk_memory]]
[[pbx-logs-check-call_quality_metrics_mos_jitter_loss_per_call]]
### pbx-regression
[[pbx-regression-check-core_call_features_after_any_code_change]]
[[pbx-regression-check-previously_fixed_bugs_and_edge_cases_reverification]]
[[pbx-regression-check-performance_baselines_comparison_against_previous_version]]
[[pbx-regression-check-security_hardening_and_vulnerability_scan_baseline]]
+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.
});
});
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"outDir": "dist",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@helpers/*": ["helpers/*"]
}
},
"include": ["tests/**/*.ts", "helpers/**/*.ts", "playwright.config.ts"],
"exclude": ["node_modules", "dist", "playwright-report", "test-results"]
}