first commit
This commit is contained in:
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login, BASE_URL } from '../../helpers/auth';
|
||||
import { markManual, markPartial } from '../../helpers/manual';
|
||||
|
||||
test.describe('pbx-regression', () => {
|
||||
test('pbx-regression-check-core_call_features_after_any_code_change', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
|
||||
testInfo.annotations.push({ type: 'testType', description: 'check' });
|
||||
markPartial('Admin portal CRUD and configuration regression can be automated; actual call-flow regression (inbound/outbound/hold/transfer) requires active SIP endpoints outside Playwright scope');
|
||||
testInfo.annotations.push({ type: 'description', description: 'Run regression checks on core call features after any code change to detect regressions' });
|
||||
|
||||
await test.step('Login and verify portal is accessible', async () => {
|
||||
await login(page);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify Extension CRUD functions are intact', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/extensions`);
|
||||
await expect(page.locator('body')).not.toContainText(/500|unexpected error/i);
|
||||
await expect(page.locator('table, .extension-list, .no-extensions').first()).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify Trunk list loads correctly', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/trunks`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
await test.step('Verify Dial Plan section is intact', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/dialplans`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
await test.step('Verify Voicemail settings load correctly', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/voicemail`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
await test.step('Verify IVR / Auto-Attendant section loads', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/ivr`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
// TODO_MANUAL: After each code merge, run SIP regression suite:
|
||||
// - Internal call test (ext→ext)
|
||||
// - Inbound DID test
|
||||
// - Outbound call test
|
||||
// - Hold/Resume, Transfer, Voicemail deposit/retrieval
|
||||
});
|
||||
|
||||
test('pbx-regression-check-previously_fixed_bugs_and_edge_cases_reverification', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
|
||||
testInfo.annotations.push({ type: 'testType', description: 'check' });
|
||||
markPartial('UI-level bug fixes can be regression-tested via Playwright; telephony-layer or server-side bug fixes require SIP clients or SSH-based verification');
|
||||
testInfo.annotations.push({ type: 'description', description: 'Verify all previously fixed bugs and known edge cases have not regressed in this release' });
|
||||
|
||||
const knownBugRoutes = [
|
||||
// Add routes that previously had UI bugs — update this list per release
|
||||
{ name: 'Extension edit page', path: '/admin/extensions' },
|
||||
{ name: 'Trunk status display', path: '/admin/trunks' },
|
||||
{ name: 'CDR export', path: '/admin/logs/cdr' },
|
||||
{ name: 'User roles', path: '/admin/users/roles' },
|
||||
];
|
||||
|
||||
await test.step('Login', async () => {
|
||||
await login(page);
|
||||
});
|
||||
|
||||
for (const route of knownBugRoutes) {
|
||||
await test.step(`Verify "${route.name}" loads without regression`, async () => {
|
||||
const response = await page.goto(`${BASE_URL}${route.path}`);
|
||||
expect(response?.status()).toBeLessThan(500);
|
||||
await expect(page.locator('body')).not.toContainText(/unexpected error|traceback|exception/i);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO_MANUAL: For each closed bug ticket, execute the reproduction steps from the ticket
|
||||
// and verify the bug is not reproducible on this release.
|
||||
});
|
||||
|
||||
test('pbx-regression-check-performance_baselines_comparison_against_previous_version', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({ type: 'automationStatus', description: 'manual' });
|
||||
testInfo.annotations.push({ type: 'testType', description: 'check' });
|
||||
markManual('Requires SIPp load tests producing comparable metrics against the previous version baseline; statistical comparison and pass/fail determination require human or scripted analysis outside the browser');
|
||||
testInfo.annotations.push({ type: 'description', description: 'Compare key performance metrics (concurrent calls, call setup time, resource usage) against the previous version baseline' });
|
||||
// Manual steps:
|
||||
// 1. Run the standard SIPp performance benchmark on the PREVIOUS version. Record: max CPS, P95 setup time, peak CPU.
|
||||
// 2. Install NEW version. Run the identical SIPp benchmark.
|
||||
// 3. Compare: max CPS diff must be ≤ 5% degradation; setup time P95 diff ≤ 10%.
|
||||
// 4. If any metric degrades > threshold, file a performance regression bug.
|
||||
// 5. Attach benchmark results (CSV + charts) to this test report.
|
||||
});
|
||||
|
||||
test('pbx-regression-check-security_hardening_and_vulnerability_scan_baseline', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({ type: 'automationStatus', description: 'partial' });
|
||||
testInfo.annotations.push({ type: 'testType', description: 'check' });
|
||||
markPartial('Security configuration checks (HTTPS, RBAC, encryption enabled) can be automated via admin portal; full vulnerability scan baseline requires external tools (OpenVAS, Nessus, Trivy) integrated into the CI pipeline');
|
||||
testInfo.annotations.push({ type: 'description', description: 'Verify security hardening settings are intact and vulnerability scan result is no worse than the previous version baseline' });
|
||||
|
||||
await test.step('Login and navigate to security overview', async () => {
|
||||
await login(page);
|
||||
await page.goto(`${BASE_URL}/admin/security`);
|
||||
});
|
||||
|
||||
await test.step('Verify HTTPS enforcement is active', async () => {
|
||||
expect(page.url()).toMatch(/^https:\/\//);
|
||||
});
|
||||
|
||||
await test.step('Verify RBAC settings page loads', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/users/roles`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
await test.step('Verify media encryption is still enabled after code change', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/security/media-encryption`);
|
||||
const pageText = await page.locator('body').textContent() ?? '';
|
||||
expect(pageText).toMatch(/srtp|tls/i);
|
||||
});
|
||||
|
||||
await test.step('Verify audit logging is still active', async () => {
|
||||
await page.goto(`${BASE_URL}/admin/security/audit`);
|
||||
await expect(page.locator('body')).not.toContainText(/500/i);
|
||||
});
|
||||
|
||||
// TODO_MANUAL / TODO_CI: Run `trivy image <new-release-image>` and compare CVE count
|
||||
// against the previous version scan. New Critical/High CVEs = regression.
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user