commit 2ebd8fb16b933e388f2a8975541f92bbded61051 Author: Juan Marquez Date: Fri May 22 11:46:43 2026 +0200 first commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b7fd592 --- /dev/null +++ b/.claude/settings.local.json @@ -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}')" + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..041239f --- /dev/null +++ b/.env.example @@ -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 diff --git a/helpers/auth.ts b/helpers/auth.ts new file mode 100644 index 0000000..686f0c6 --- /dev/null +++ b/helpers/auth.ts @@ -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 { + 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 { + 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 }; diff --git a/helpers/manual.ts b/helpers/manual.ts new file mode 100644 index 0000000..ed7da5e --- /dev/null +++ b/helpers/manual.ts @@ -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' }); +} diff --git a/helpers/writeResult.ts b/helpers/writeResult.ts new file mode 100644 index 0000000..e5204af --- /dev/null +++ b/helpers/writeResult.ts @@ -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, + label = 'recorded-output', +): Promise { + 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', + }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a7227cb --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e879cb6 --- /dev/null +++ b/playwright.config.ts @@ -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', +}); diff --git a/report/test-plan.html b/report/test-plan.html new file mode 100644 index 0000000..b75fd8b --- /dev/null +++ b/report/test-plan.html @@ -0,0 +1,963 @@ + + + + + + QA Automation Test Plan Report + + + + + + + + + +
+ + +

Overview

+
+
+
Total Tests
+
86
+
Across 18 groups
+
+
+
+
Automated
+
34
+
39.5% of total
+
+
+
+
Partial
+
16
+
18.6% of total
+
+
+
+
Manual
+
36
+
41.9% of total
+
+
+
+ + +

Charts

+
+ +
+

Automation Distribution

+
+ +
+
+
Automated 34
+
Partial 16
+
Manual 36
+
+
+ + +
+

Automation by Group

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
86 / 86 tests
+
+ + +
+
+ + + + + + + + + + + + + + +
#Test IDGroupModuleActionDescriptionStatusSpec File
+
+ + No tests match the current filters. +
+
+
+ +
+ +
+ Generated automatically by QA Automation Framework · Playwright + TypeScript + MCP +
+ + + + diff --git a/testplans/core/index.txt b/testplans/core/index.txt new file mode 100644 index 0000000..164fe2e --- /dev/null +++ b/testplans/core/index.txt @@ -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]] diff --git a/testplans/pbx/index.txt b/testplans/pbx/index.txt new file mode 100644 index 0000000..25aaf7a --- /dev/null +++ b/testplans/pbx/index.txt @@ -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]] diff --git a/tests/core/documentation.spec.ts b/tests/core/documentation.spec.ts new file mode 100644 index 0000000..a5a0914 --- /dev/null +++ b/tests/core/documentation.spec.ts @@ -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 = {}; + + 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 = {}; + + 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); + }); +}); diff --git a/tests/core/features.spec.ts b/tests/core/features.spec.ts new file mode 100644 index 0000000..2c2fbcf --- /dev/null +++ b/tests/core/features.spec.ts @@ -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 = {}; + + 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 = {}; + 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(); + }); + }); +}); diff --git a/tests/core/installation.spec.ts b/tests/core/installation.spec.ts new file mode 100644 index 0000000..c5cd871 --- /dev/null +++ b/tests/core/installation.spec.ts @@ -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 + // 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); + }); + }); +}); diff --git a/tests/core/interoperability.spec.ts b/tests/core/interoperability.spec.ts new file mode 100644 index 0000000..60793f9 --- /dev/null +++ b/tests/core/interoperability.spec.ts @@ -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); + }); + }); +}); diff --git a/tests/core/repository.spec.ts b/tests/core/repository.spec.ts new file mode 100644 index 0000000..62821c9 --- /dev/null +++ b/tests/core/repository.spec.ts @@ -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(); + }); + }); +}); diff --git a/tests/core/security.spec.ts b/tests/core/security.spec.ts new file mode 100644 index 0000000..448dfc7 --- /dev/null +++ b/tests/core/security.spec.ts @@ -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 = {}; + + 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 + // 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 ` 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 :443` shows TLS 1.2+ and strong cipher suite. + }); +}); diff --git a/tests/core/upgrade.spec.ts b/tests/core/upgrade.spec.ts new file mode 100644 index 0000000..0f856db --- /dev/null +++ b/tests/core/upgrade.spec.ts @@ -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. + }); +}); diff --git a/tests/core/virtualization.spec.ts b/tests/core/virtualization.spec.ts new file mode 100644 index 0000000..4bd3ad0 --- /dev/null +++ b/tests/core/virtualization.spec.ts @@ -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 = {}; + + 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 '{"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. + }); +}); diff --git a/tests/pbx/availability.spec.ts b/tests/pbx/availability.spec.ts new file mode 100644 index 0000000..16e09ac --- /dev/null +++ b/tests/pbx/availability.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/calls-security.spec.ts b/tests/pbx/calls-security.spec.ts new file mode 100644 index 0000000..5ab595e --- /dev/null +++ b/tests/pbx/calls-security.spec.ts @@ -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(); + } + }); + }); +}); diff --git a/tests/pbx/calls.spec.ts b/tests/pbx/calls.spec.ts new file mode 100644 index 0000000..cb533b1 --- /dev/null +++ b/tests/pbx/calls.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/codecs.spec.ts b/tests/pbx/codecs.spec.ts new file mode 100644 index 0000000..9296e2f --- /dev/null +++ b/tests/pbx/codecs.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/configuration.spec.ts b/tests/pbx/configuration.spec.ts new file mode 100644 index 0000000..4456841 --- /dev/null +++ b/tests/pbx/configuration.spec.ts @@ -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); + }); + }); +}); diff --git a/tests/pbx/extensions.spec.ts b/tests/pbx/extensions.spec.ts new file mode 100644 index 0000000..de53ded --- /dev/null +++ b/tests/pbx/extensions.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/logs.spec.ts b/tests/pbx/logs.spec.ts new file mode 100644 index 0000000..fb51efb --- /dev/null +++ b/tests/pbx/logs.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/management.spec.ts b/tests/pbx/management.spec.ts new file mode 100644 index 0000000..7a56782 --- /dev/null +++ b/tests/pbx/management.spec.ts @@ -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); + }); + }); +}); diff --git a/tests/pbx/performance.spec.ts b/tests/pbx/performance.spec.ts new file mode 100644 index 0000000..531435f --- /dev/null +++ b/tests/pbx/performance.spec.ts @@ -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. + }); +}); diff --git a/tests/pbx/regression.spec.ts b/tests/pbx/regression.spec.ts new file mode 100644 index 0000000..67af443 --- /dev/null +++ b/tests/pbx/regression.spec.ts @@ -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 ` and compare CVE count + // against the previous version scan. New Critical/High CVEs = regression. + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9f6a9b6 --- /dev/null +++ b/tsconfig.json @@ -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"] +}