webcomponent: Added missing and necessary js file Update .gitignore to specify backend public directory exclusion webcomponent: Add error handling for invalid base URL in OpenViduMeet component webcomponent: Update Jest configuration for improved testing setup webcomponent: Enhance iframe attribute tests and add support for optional query parameters webcomponent: Refactor documentation copying in build_webcomponent_doc function for improved readability and add absolute path resolution Add E2EE_KEY property to WebComponentProperty enum for end-to-end encryption support meet.sh: Enhance build_rest_api_doc function with output file handling and user confirmation for overwriting frontend: replace removeRoomSecretGuard with removeQueryParamsGuard for enhanced query parameter management frontend: add E2EE key handling in room service and update query params guard Updated pnpm-lock.yaml Enables end-to-end encryption (E2EE) Adds E2EE functionality to meeting rooms. Significant changes: - Allows encryption of the participant name - Introduces setting and getting E2EE keys - Ensures recording is disabled when encryption is enabled webcomponent: Added e2e test for checking the e2ee funcionality frontend: Sanitize participant name before request for a token fix: clean up formatting in openvidu-meet.code-workspace
878 lines
33 KiB
TypeScript
878 lines
33 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
import { MeetRecordingAccess } from '../../../../typings/src/room-config';
|
||
import { MEET_TESTAPP_URL } from '../config';
|
||
import {
|
||
closeMoreOptionsMenu,
|
||
countElementsInIframe,
|
||
createTestRoom,
|
||
deleteAllRecordings,
|
||
deleteAllRooms,
|
||
interactWithElementInIframe,
|
||
joinRoomAs,
|
||
leaveRoom,
|
||
openMoreOptionsMenu,
|
||
prepareForJoiningRoom,
|
||
updateRoomConfig,
|
||
waitForElementInIframe
|
||
} from '../helpers/function-helpers';
|
||
|
||
let subscribedToAppErrors = false;
|
||
|
||
test.describe('E2EE UI Tests', () => {
|
||
let roomId: string;
|
||
let participantName: string;
|
||
|
||
// ==========================================
|
||
// SETUP & TEARDOWN
|
||
// ==========================================
|
||
|
||
test.beforeAll(async () => {
|
||
// Create a test room before all tests
|
||
roomId = await createTestRoom('test-room-e2ee');
|
||
});
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
if (!subscribedToAppErrors) {
|
||
page.on('console', (msg) => {
|
||
const type = msg.type();
|
||
const tag = type === 'error' ? 'ERROR' : type === 'warning' ? 'WARNING' : 'LOG';
|
||
console.log('[' + tag + ']', msg.text());
|
||
});
|
||
subscribedToAppErrors = true;
|
||
}
|
||
|
||
participantName = `P-${Math.random().toString(36).substring(2, 9)}`;
|
||
});
|
||
|
||
test.afterAll(async ({ browser }) => {
|
||
const tempContext = await browser.newContext();
|
||
const tempPage = await tempContext.newPage();
|
||
await deleteAllRooms(tempPage);
|
||
await deleteAllRecordings(tempPage);
|
||
await tempContext.close();
|
||
await tempPage.close();
|
||
});
|
||
|
||
// ==========================================
|
||
// E2EE LOBBY UI TESTS
|
||
// ==========================================
|
||
|
||
test.describe('E2EE Lobby Elements', () => {
|
||
test('should show E2EE key input and badge in lobby when E2EE is enabled', async ({ page }) => {
|
||
// Enable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
await page.click('#join-as-speaker');
|
||
|
||
const component = page.locator('openvidu-meet');
|
||
await expect(component).toBeVisible();
|
||
|
||
// Wait for participant name input
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
|
||
// Check that E2EE badge is visible
|
||
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'visible' });
|
||
await expect(e2eeBadge).toBeVisible();
|
||
await expect(e2eeBadge).toContainText('end-to-end encrypted');
|
||
|
||
// Check that E2EE key input is visible
|
||
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
|
||
state: 'visible'
|
||
});
|
||
await expect(e2eeKeyInput).toBeVisible();
|
||
|
||
// Check that the input has correct attributes
|
||
await expect(e2eeKeyInput).toHaveAttribute('type', 'password');
|
||
await expect(e2eeKeyInput).toHaveAttribute('required');
|
||
});
|
||
|
||
test('should hide E2EE elements in lobby when E2EE is disabled', async ({ page }) => {
|
||
// Disable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: false }
|
||
});
|
||
|
||
await page.goto(MEET_TESTAPP_URL);
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
await page.click('#join-as-speaker');
|
||
|
||
const component = page.locator('openvidu-meet');
|
||
await expect(component).toBeVisible();
|
||
|
||
// Wait for participant name input
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
|
||
// Check that E2EE badge is hidden
|
||
const e2eeBadge = await waitForElementInIframe(page, '.encryption-badge', { state: 'hidden' });
|
||
await expect(e2eeBadge).toBeHidden();
|
||
|
||
// Check that E2EE key input is hidden
|
||
const e2eeKeyInput = await waitForElementInIframe(page, '#participant-e2eekey-input', {
|
||
state: 'hidden'
|
||
});
|
||
await expect(e2eeKeyInput).toBeHidden();
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// E2EE MEETING TESTS
|
||
// ==========================================
|
||
|
||
test.describe('E2EE in Meeting', () => {
|
||
test.afterEach(async ({ page }) => {
|
||
try {
|
||
await leaveRoom(page);
|
||
} catch (error) {
|
||
// Ignore errors if already left
|
||
}
|
||
});
|
||
|
||
test('should allow participants to see and hear each other with correct E2EE key', async ({
|
||
page,
|
||
context
|
||
}) => {
|
||
// Enable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
// Create a second page for participant 2
|
||
const page2 = await context.newPage();
|
||
|
||
// Participant 1 joins with E2EE key
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
await page.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participantName
|
||
});
|
||
|
||
// Fill E2EE key
|
||
const e2eeKey = 'test-encryption-key-123';
|
||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey
|
||
});
|
||
|
||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||
|
||
// Participant 2 joins with same E2EE key
|
||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||
await page2.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant2Name
|
||
});
|
||
|
||
// Fill same E2EE key
|
||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey
|
||
});
|
||
|
||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||
|
||
// Wait a bit for media to flow
|
||
await page.waitForTimeout(2000);
|
||
|
||
// Check that both participants can see each other's video elements
|
||
const videoCount1 = await countElementsInIframe(page, '.OV_video-element');
|
||
expect(videoCount1).toBeGreaterThanOrEqual(2);
|
||
|
||
const videoCount2 = await countElementsInIframe(page2, '.OV_video-element');
|
||
expect(videoCount2).toBeGreaterThanOrEqual(2);
|
||
|
||
// Check that no encryption error poster is shown
|
||
const encryptionError1 = await waitForElementInIframe(page, '.encryption-error-poster', {
|
||
state: 'hidden'
|
||
});
|
||
await expect(encryptionError1).toBeHidden();
|
||
|
||
const encryptionError2 = await waitForElementInIframe(page2, '.encryption-error-poster', {
|
||
state: 'hidden'
|
||
});
|
||
await expect(encryptionError2).toBeHidden();
|
||
|
||
// Expect video to be flowing (by checking the video element has video tracks)
|
||
const videoElements = await waitForElementInIframe(page, '.OV_video-element', {
|
||
state: 'visible',
|
||
all: true
|
||
});
|
||
for (const videoElement of videoElements) {
|
||
const videoTracks = await videoElement.evaluate((el) => (el as any).srcObject?.getVideoTracks());
|
||
expect(videoTracks.length).toBeGreaterThan(0);
|
||
}
|
||
|
||
// Cleanup participant 2
|
||
await leaveRoom(page2);
|
||
await page2.close();
|
||
});
|
||
|
||
test('should show encryption error poster when using wrong E2EE key', async ({ page, context }) => {
|
||
// Enable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
// Create a second page for participant 2
|
||
const page2 = await context.newPage();
|
||
|
||
// Participant 1 joins with E2EE key
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
await page.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participantName
|
||
});
|
||
|
||
// Fill E2EE key
|
||
const e2eeKey1 = 'correct-key-abc';
|
||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey1
|
||
});
|
||
|
||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||
|
||
// Participant 2 joins with DIFFERENT E2EE key
|
||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||
await page2.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant2Name
|
||
});
|
||
|
||
// Fill DIFFERENT E2EE key
|
||
const e2eeKey2 = 'wrong-key-xyz';
|
||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey2
|
||
});
|
||
|
||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||
|
||
// Wait for encryption error to be detected
|
||
await page.waitForTimeout(3000);
|
||
|
||
// Check that encryption error poster is shown on both sides
|
||
// Each participant should see an encryption error for the other's video
|
||
const videoPosterCount = await countElementsInIframe(page, '.encryption-error-poster');
|
||
|
||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||
expect(videoPosterCount).toBe(2);
|
||
|
||
const videoPosterCount2 = await countElementsInIframe(page2, '.encryption-error-poster');
|
||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||
expect(videoPosterCount2).toBe(2);
|
||
|
||
// Add additional participant with correct key to verify they can see/hear each other
|
||
const page3 = await context.newPage();
|
||
const participant3Name = `P3-${Math.random().toString(36).substring(2, 9)}`;
|
||
await prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId);
|
||
await page3.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page3, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant3Name
|
||
});
|
||
|
||
// Fill CORRECT E2EE key
|
||
await interactWithElementInIframe(page3, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey1
|
||
});
|
||
|
||
await interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page3, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-session', { state: 'visible' });
|
||
|
||
// Wait a bit for media to flow
|
||
await page3.waitForTimeout(2000);
|
||
|
||
// Check that participant 3 can see participant 1's video
|
||
const videoCount3 = await countElementsInIframe(page3, '.OV_video-element');
|
||
expect(videoCount3).toBeGreaterThanOrEqual(2);
|
||
|
||
const videoPosterCount3 = await countElementsInIframe(page3, '.encryption-error-poster');
|
||
//! FIXME: Temporarily expecting 2 posters due to audio and video streams (needs to be fixed in ov-components)
|
||
expect(videoPosterCount3).toBe(2);
|
||
|
||
// Cleanup participant 2
|
||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||
await Promise.all([page2.close(), page3.close()]);
|
||
});
|
||
|
||
test('should decrypt participant names and chat messages with correct E2EE key', async ({ page, context }) => {
|
||
// Enable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
const e2eeKey = 'shared-encryption-key-456';
|
||
const participant1Name = `Alice-${Math.random().toString(36).substring(2, 9)}`;
|
||
const participant2Name = `Bob-${Math.random().toString(36).substring(2, 9)}`;
|
||
|
||
// Participant 1 joins with E2EE key
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
await page.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant1Name
|
||
});
|
||
|
||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey
|
||
});
|
||
|
||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||
|
||
// Participant 2 joins with same E2EE key
|
||
const page2 = await context.newPage();
|
||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||
await page2.click('#join-as-speaker');
|
||
|
||
await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant2Name
|
||
});
|
||
|
||
await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey
|
||
});
|
||
|
||
await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
|
||
await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
|
||
|
||
// Wait for participants to connect
|
||
await page.waitForTimeout(2000);
|
||
|
||
// ===== CHECK PARTICIPANT NAMES IN VIDEO GRID =====
|
||
// Participant 1 should see Participant 2's name decrypted
|
||
const participantNameElements = await Promise.all([
|
||
waitForElementInIframe(page, '#participant-name', {
|
||
state: 'attached',
|
||
all: true
|
||
}),
|
||
waitForElementInIframe(page2, '#participant-name', {
|
||
state: 'attached',
|
||
all: true
|
||
})
|
||
]);
|
||
|
||
for (const participantNameElement of participantNameElements.flat()) {
|
||
const name = await participantNameElement.evaluate((el) => el.textContent);
|
||
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
|
||
expect(name).not.toContain('*');
|
||
}
|
||
|
||
// ===== CHECK NAMES IN PARTICIPANTS PANEL =====
|
||
// Open participants panel
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
|
||
]);
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-participants-panel', { state: 'visible' }),
|
||
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'visible' })
|
||
]);
|
||
// Check that both names are visible and decrypted in the panel
|
||
const participantsPanelNames = await Promise.all([
|
||
waitForElementInIframe(page, '.participant-item-name span', {
|
||
state: 'attached',
|
||
all: true
|
||
}),
|
||
waitForElementInIframe(page2, '.participant-item-name span', {
|
||
state: 'attached',
|
||
all: true
|
||
})
|
||
]);
|
||
|
||
for (const participantPanelName of participantsPanelNames.flat()) {
|
||
const name = await participantPanelName.evaluate((el) => el.textContent);
|
||
expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
|
||
expect(name).not.toContain('*');
|
||
}
|
||
|
||
// Close participants panel
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
|
||
]);
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-participants-panel', { state: 'hidden' }),
|
||
waitForElementInIframe(page2, 'ov-participants-panel', { state: 'hidden' })
|
||
]);
|
||
|
||
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
|
||
// Open settings panel
|
||
await Promise.all([openMoreOptionsMenu(page), openMoreOptionsMenu(page2)]);
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#toolbar-settings-btn', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#toolbar-settings-btn', { action: 'click' })
|
||
]);
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-settings-panel', { state: 'visible' }),
|
||
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'visible' })
|
||
]);
|
||
|
||
// Check that own name is visible and decrypted
|
||
const ownNameInputs = await Promise.all([
|
||
waitForElementInIframe(page, '#participant-name-input', {
|
||
state: 'visible'
|
||
}),
|
||
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' })
|
||
]);
|
||
|
||
const ownName1 = await ownNameInputs[0].evaluate((el: HTMLInputElement) => el.value);
|
||
const ownName2 = await ownNameInputs[1].evaluate((el: HTMLInputElement) => el.value);
|
||
expect(ownName1).toBe(participant1Name);
|
||
expect(ownName1).not.toContain('*');
|
||
expect(ownName2).toBe(participant2Name);
|
||
expect(ownName2).not.toContain('*');
|
||
|
||
// Close settings panel
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '.panel-close-button', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '.panel-close-button', { action: 'click' })
|
||
]);
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-settings-panel', { state: 'hidden' }),
|
||
waitForElementInIframe(page2, 'ov-settings-panel', { state: 'hidden' })
|
||
]);
|
||
await Promise.all([closeMoreOptionsMenu(page), closeMoreOptionsMenu(page2)]);
|
||
|
||
// ===== CHECK CHAT MESSAGES =====
|
||
// Open chat
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#chat-panel-btn', { action: 'click' })
|
||
]);
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' }),
|
||
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
|
||
]);
|
||
|
||
// ===== MESSAGE: PARTICIPANT 1 → PARTICIPANT 2 =====
|
||
const testMessage1 = `Hello from ${participant1Name}!`;
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: testMessage1 }),
|
||
waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
|
||
]);
|
||
|
||
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
|
||
|
||
// Wait for message to be sent
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Open chat on page 2
|
||
const chatMessages2 = await waitForElementInIframe(page2, '.chat-message', { state: 'visible' });
|
||
|
||
// Verify message content
|
||
const messageText2 = await chatMessages2.evaluate((el) => el.textContent || '');
|
||
expect(messageText2).toContain(testMessage1);
|
||
expect(messageText2).not.toContain('*');
|
||
|
||
// ===== MESSAGE: PARTICIPANT 2 → PARTICIPANT 1 =====
|
||
const testMessage2 = `Hi from ${participant2Name}!`;
|
||
|
||
// Send message in page2 iframe
|
||
await interactWithElementInIframe(page2, '#chat-input', { action: 'fill', value: testMessage2 });
|
||
await interactWithElementInIframe(page2, '#send-btn', { action: 'click' });
|
||
|
||
// Wait briefly for message delivery
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Wait for message on participant 1’s side
|
||
const chatMessages1 = await waitForElementInIframe(page, '.chat-message', { state: 'visible' });
|
||
|
||
// Collect all chat messages in the chat panel
|
||
const allMessages1 = await chatMessages1.evaluate((el) =>
|
||
Array.from(el.closest('ov-chat-panel')?.querySelectorAll('.chat-message') || []).map(
|
||
(e) => e.textContent || ''
|
||
)
|
||
);
|
||
|
||
// Verify received message
|
||
expect(allMessages1.join(' ')).toContain(testMessage2);
|
||
expect(allMessages1.join(' ')).not.toContain('*');
|
||
|
||
// Cleanup
|
||
await leaveRoom(page2);
|
||
await page2.close();
|
||
});
|
||
|
||
test('should show masked names and unreadable messages for participant with wrong E2EE key', async ({
|
||
page,
|
||
context
|
||
}) => {
|
||
// Enable E2EE
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
const correctKey = 'correct-shared-key-789';
|
||
const wrongKey = 'wrong-key-999';
|
||
const participant1Name = `Charlie-${Math.random().toString(36).substring(2, 9)}`;
|
||
const participant2Name = `David-${Math.random().toString(36).substring(2, 9)}`;
|
||
const participant3Name = `Eve-${Math.random().toString(36).substring(2, 9)}`;
|
||
const [page2, page3] = await Promise.all([context.newPage(), context.newPage()]);
|
||
|
||
// Prepare for all participants to join the room
|
||
await Promise.all([
|
||
prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId),
|
||
prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId),
|
||
prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId)
|
||
]);
|
||
|
||
// Join as speaker in all pages
|
||
await Promise.all([
|
||
page.click('#join-as-speaker'),
|
||
page2.click('#join-as-speaker'),
|
||
page3.click('#join-as-speaker')
|
||
]);
|
||
|
||
// Wait for name and E2EE key inputs to be visible in all pages
|
||
await Promise.all([
|
||
waitForElementInIframe(page, '#participant-name-input', { state: 'visible' }),
|
||
waitForElementInIframe(page, '#participant-e2eekey-input', { state: 'visible' }),
|
||
waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' }),
|
||
waitForElementInIframe(page2, '#participant-e2eekey-input', { state: 'visible' }),
|
||
waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' }),
|
||
waitForElementInIframe(page3, '#participant-e2eekey-input', { state: 'visible' })
|
||
]);
|
||
|
||
// Fill participant names
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant1Name
|
||
}),
|
||
interactWithElementInIframe(page2, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant2Name
|
||
}),
|
||
interactWithElementInIframe(page3, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participant3Name
|
||
})
|
||
]);
|
||
|
||
// Fill E2EE keys (two correct, one wrong)
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
|
||
interactWithElementInIframe(page2, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
|
||
interactWithElementInIframe(page3, '#participant-e2eekey-input', { action: 'fill', value: wrongKey })
|
||
]);
|
||
|
||
// Join all participants
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' }),
|
||
interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' })
|
||
]);
|
||
|
||
// Wait for prejoin page in all pages
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' }),
|
||
waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' }),
|
||
waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' })
|
||
]);
|
||
|
||
// Click join button in all pages
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#join-button', { action: 'click' }),
|
||
interactWithElementInIframe(page2, '#join-button', { action: 'click' }),
|
||
interactWithElementInIframe(page3, '#join-button', { action: 'click' })
|
||
]);
|
||
|
||
// Wait for session to be visible in all pages
|
||
await Promise.all([
|
||
waitForElementInIframe(page, 'ov-session', { state: 'visible' }),
|
||
waitForElementInIframe(page2, 'ov-session', { state: 'visible' }),
|
||
waitForElementInIframe(page3, 'ov-session', { state: 'visible' })
|
||
]);
|
||
|
||
// Wait for participants to connect
|
||
await page.waitForTimeout(1000);
|
||
|
||
// Check that participant 3 sees encryption error posters for others
|
||
// ===== CHECK MASKED NAMES IN VIDEO GRID FOR PARTICIPANT 3 =====
|
||
const participantNameElements3 = await waitForElementInIframe(
|
||
page3,
|
||
'#layout .participant-name-container #participant-name',
|
||
{
|
||
state: 'attached',
|
||
all: true
|
||
}
|
||
);
|
||
const participantNames3 = await Promise.all(
|
||
participantNameElements3.map((el) => el.evaluate((e) => e.textContent))
|
||
);
|
||
|
||
console.log('Participant Names Seen by Participant 3:', participantNames3);
|
||
console.log('Expected: 3 names (own + 2 masked), got:', participantNames3.length);
|
||
|
||
// Should have exactly 3 participants
|
||
expect(participantNames3.length).toBe(3);
|
||
|
||
// Should NOT all be masked (own name should be visible)
|
||
expect(participantNames3.every((name) => name?.includes('******'))).toBeFalsy();
|
||
|
||
// Should have exactly 2 masked names
|
||
const maskedNames = participantNames3.filter((name) => name?.includes('******'));
|
||
expect(maskedNames.length).toBe(2);
|
||
|
||
// Should see own name
|
||
expect(participantNames3).toContain(participant3Name);
|
||
|
||
// Should NOT see the actual names of P1 and P2
|
||
expect(participantNames3.join(' ')).not.toContain(participant1Name);
|
||
expect(participantNames3.join(' ')).not.toContain(participant2Name);
|
||
|
||
// ===== CHECK MASKED NAMES IN PARTICIPANTS PANEL =====
|
||
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'visible' });
|
||
|
||
const participantsPanelNames3 = await waitForElementInIframe(page3, '.participant-name-text', {
|
||
state: 'visible',
|
||
all: true
|
||
});
|
||
const panelNamesText3 = await Promise.all(
|
||
participantsPanelNames3.map((el) => el.evaluate((e) => e.textContent))
|
||
);
|
||
|
||
console.log('Panel Names Seen by Participant 3:', panelNamesText3);
|
||
console.log('Expected: 3 names (own + 2 masked), got:', panelNamesText3.length);
|
||
|
||
// Should have exactly 3 participants in panel
|
||
expect(panelNamesText3.length).toBe(3);
|
||
|
||
// Should NOT all be masked (own name should be visible)
|
||
expect(panelNamesText3.every((name) => name?.includes('******'))).toBeFalsy();
|
||
|
||
// Should have exactly 2 masked names
|
||
const maskedPanelNames = panelNamesText3.filter((name) => name?.includes('******'));
|
||
expect(maskedPanelNames.length).toBe(2);
|
||
|
||
// Should see own name
|
||
expect(panelNamesText3).toContain(participant3Name);
|
||
|
||
// Should NOT see the actual names of P1 and P2
|
||
expect(panelNamesText3.join(' ')).not.toContain(participant1Name);
|
||
expect(panelNamesText3.join(' ')).not.toContain(participant2Name);
|
||
|
||
await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'hidden' });
|
||
|
||
// ===== CHECK OWN NAME IN SETTINGS PANEL =====
|
||
await openMoreOptionsMenu(page3);
|
||
await interactWithElementInIframe(page3, '#toolbar-settings-btn', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'visible' });
|
||
|
||
const ownNameInput3 = await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
|
||
const ownName3 = await ownNameInput3.evaluate((el: HTMLInputElement) => el.value);
|
||
expect(ownName3).toBe(participant3Name);
|
||
expect(ownName3).not.toContain('******');
|
||
|
||
await interactWithElementInIframe(page3, '.panel-close-button', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'hidden' });
|
||
await closeMoreOptionsMenu(page3);
|
||
|
||
// ===== SEND MESSAGE FROM PARTICIPANT 1 =====
|
||
const secretMessage = `Secret message from ${participant1Name}`;
|
||
await Promise.all([
|
||
interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
|
||
waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' })
|
||
]);
|
||
|
||
// Send message
|
||
await interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: secretMessage });
|
||
await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
|
||
|
||
// Wait for message to be sent and received
|
||
await Promise.all([
|
||
waitForElementInIframe(page2, '#chat-panel-btn .mat-badge-content', { state: 'visible' }),
|
||
waitForElementInIframe(page3, '#chat-panel-btn .mat-badge-content', { state: 'visible' })
|
||
]);
|
||
|
||
// ===== CHECK CHAT MESSAGES ARE UNREADABLE =====
|
||
await interactWithElementInIframe(page3, '#chat-panel-btn', { action: 'click' });
|
||
await waitForElementInIframe(page3, 'ov-chat-panel', { state: 'visible' });
|
||
|
||
await page3.waitForTimeout(1000);
|
||
|
||
const chatMessagesCount = await countElementsInIframe(page3, '.chat-message');
|
||
expect(chatMessagesCount).toBeGreaterThan(0);
|
||
|
||
const chatMessages3 = await waitForElementInIframe(page3, '.chat-message', {
|
||
state: 'visible',
|
||
all: true
|
||
});
|
||
const messagesText3 = await Promise.all(chatMessages3.map((el) => el.evaluate((e) => e.textContent)));
|
||
|
||
console.log('Chat Messages Seen by Participant 3:', messagesText3);
|
||
console.log('Expected: All messages masked, got:', messagesText3.length, 'messages');
|
||
|
||
// All messages should contain the mask
|
||
expect(messagesText3.every((text) => text?.includes('******'))).toBeTruthy();
|
||
|
||
// Should NOT contain the actual secret message
|
||
expect(messagesText3.join(' ')).not.toContain(secretMessage);
|
||
|
||
// ===== VERIFY PARTICIPANTS 1 AND 2 CAN STILL SEE EACH OTHER =====
|
||
const participantNameElements1 = await waitForElementInIframe(page, '.participant-name', {
|
||
state: 'visible',
|
||
all: true
|
||
});
|
||
const participantNames1 = await Promise.all(
|
||
participantNameElements1.map((el) => el.evaluate((e) => e.textContent))
|
||
);
|
||
expect(participantNames1.join(' ')).toContain(participant2Name);
|
||
|
||
const participantNameElements2 = await waitForElementInIframe(page2, '.participant-name', {
|
||
state: 'visible',
|
||
all: true
|
||
});
|
||
const participantNames2 = await Promise.all(
|
||
participantNameElements2.map((el) => el.evaluate((e) => e.textContent))
|
||
);
|
||
expect(participantNames2.join(' ')).toContain(participant1Name);
|
||
|
||
// Cleanup
|
||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||
await Promise.all([page2.close(), page3.close()]);
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// E2EE AND RECORDING INTERACTION TESTS
|
||
// ==========================================
|
||
|
||
test.describe('E2EE and Recording', () => {
|
||
test.afterEach(async ({ page }) => {
|
||
try {
|
||
await leaveRoom(page, 'moderator');
|
||
} catch (error) {
|
||
// Ignore errors if already left
|
||
}
|
||
});
|
||
|
||
test('should hide recording button when E2EE is enabled', async ({ page }) => {
|
||
// Enable E2EE (which should auto-disable recording)
|
||
await updateRoomConfig(roomId, {
|
||
chat: { enabled: true },
|
||
recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
|
||
virtualBackground: { enabled: true },
|
||
e2ee: { enabled: true }
|
||
});
|
||
|
||
await page.goto(MEET_TESTAPP_URL);
|
||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||
|
||
// Join as moderator to access recording controls
|
||
await page.click('#join-as-moderator');
|
||
const component = page.locator('openvidu-meet');
|
||
await expect(component).toBeVisible();
|
||
|
||
// Fill participant name
|
||
await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#participant-name-input', {
|
||
action: 'fill',
|
||
value: participantName
|
||
});
|
||
|
||
// Fill E2EE key
|
||
const e2eeKey = 'test-key-recording';
|
||
await interactWithElementInIframe(page, '#participant-e2eekey-input', {
|
||
action: 'fill',
|
||
value: e2eeKey
|
||
});
|
||
|
||
await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
|
||
|
||
// Wait for prejoin page and join
|
||
await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
|
||
await interactWithElementInIframe(page, '#join-button', { action: 'click' });
|
||
await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
|
||
|
||
// Open more options menu
|
||
await openMoreOptionsMenu(page);
|
||
|
||
// Check that recording button is not visible
|
||
const recordingButton = await waitForElementInIframe(page, '#recording-btn', { state: 'hidden' });
|
||
await expect(recordingButton).toBeHidden();
|
||
|
||
await closeMoreOptionsMenu(page);
|
||
|
||
// Also check that recording activities panel is not available
|
||
const activitiesButton = await waitForElementInIframe(page, '#activities-panel-btn', { state: 'hidden' });
|
||
await expect(activitiesButton).toBeHidden();
|
||
});
|
||
});
|
||
});
|