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
407 lines
16 KiB
TypeScript
407 lines
16 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
|
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
|
import { WebComponentProperty } from '@openvidu-meet/typings';
|
|
import '../../src/index';
|
|
|
|
describe('OpenViduMeet WebComponent Attributes', () => {
|
|
let component: OpenViduMeet;
|
|
|
|
beforeEach(() => {
|
|
component = document.createElement('openvidu-meet') as OpenViduMeet;
|
|
document.body.appendChild(component);
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.removeChild(component);
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
// ==========================================
|
|
// IFRAME SETUP
|
|
// ==========================================
|
|
describe('Iframe Configuration', () => {
|
|
it('should render iframe with correct media permissions', () => {
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe).not.toBeNull();
|
|
|
|
const allowAttribute = iframe?.getAttribute('allow');
|
|
expect(allowAttribute).toContain('camera');
|
|
expect(allowAttribute).toContain('microphone');
|
|
expect(allowAttribute).toContain('display-capture');
|
|
expect(allowAttribute).toContain('fullscreen');
|
|
expect(allowAttribute).toContain('autoplay');
|
|
expect(allowAttribute).toContain('compute-pressure');
|
|
});
|
|
|
|
it('should have iframe ready in shadow DOM', () => {
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe).toBeInstanceOf(HTMLIFrameElement);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// REQUIRED ATTRIBUTES (room-url | recording-url)
|
|
// ==========================================
|
|
describe('Required Attributes', () => {
|
|
it('should reject iframe src when both room-url and recording-url are missing', () => {
|
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Trigger updateIframeSrc manually
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
|
|
expect(iframe).toBeDefined();
|
|
expect(iframe?.src).toBeFalsy();
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
it('should set iframe src when room-url attribute is provided', () => {
|
|
const roomUrl = 'https://example.com/room/testRoom-123';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe?.src).toBe(roomUrl);
|
|
});
|
|
|
|
it('should set iframe src when recording-url attribute is provided', () => {
|
|
const recordingUrl = 'https://example.com/recordings/recording-abc-123';
|
|
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe?.src).toBe(recordingUrl);
|
|
});
|
|
|
|
it('should prefer room-url over recording-url when both are provided', () => {
|
|
const roomUrl = 'https://example.com/room/testRoom-123';
|
|
const recordingUrl = 'https://example.com/recordings/recording-abc-123';
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe?.src).toBe(roomUrl);
|
|
});
|
|
|
|
it('should extract origin from room-url and set as target origin', () => {
|
|
const domain = 'https://example.com';
|
|
const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
expect((component as any).targetIframeOrigin).toBe(domain);
|
|
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
|
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
|
});
|
|
|
|
it('should extract origin from recording-url and set as target origin', () => {
|
|
const domain = 'https://recordings.example.com';
|
|
const recordingUrl = `${domain}/recordings/recording-abc-123`;
|
|
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
expect((component as any).targetIframeOrigin).toBe(domain);
|
|
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
|
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
|
});
|
|
|
|
it('should update iframe src when room-url attribute changes', () => {
|
|
const roomUrl1 = 'https://example.com/room/room-1';
|
|
const roomUrl2 = 'https://example.com/room/room-2';
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl1);
|
|
(component as any).updateIframeSrc();
|
|
|
|
let iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe?.src).toBe(roomUrl1);
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl2);
|
|
(component as any).updateIframeSrc();
|
|
|
|
iframe = component.shadowRoot?.querySelector('iframe');
|
|
expect(iframe?.src).toBe(roomUrl2);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// OPTIONAL ATTRIBUTES AS QUERY PARAMETERS
|
|
// ==========================================
|
|
describe('Optional Attributes as Query Parameters', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
|
|
it('should add participant-name as query parameter', () => {
|
|
const participantName = 'John Doe';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
|
});
|
|
|
|
it('should add e2ee-key as query parameter', () => {
|
|
const e2eeKey = 'secret-encryption-key-123';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
|
});
|
|
|
|
it('should add leave-redirect-url as query parameter', () => {
|
|
const redirectUrl = 'https://example.com/goodbye';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
|
});
|
|
|
|
it('should add show-only-recordings as query parameter', () => {
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
|
|
});
|
|
|
|
it('should add multiple optional attributes as query parameters', () => {
|
|
const participantName = 'Jane Smith';
|
|
const e2eeKey = 'encryption-key-456';
|
|
const redirectUrl = 'https://example.com/thanks';
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
|
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
|
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
|
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'false');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
|
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
|
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
|
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('false');
|
|
});
|
|
|
|
it('should NOT add room-url or recording-url as query parameters', () => {
|
|
const roomUrl = 'https://example.com/room/testRoom?secret=abc';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// room-url should not be in query params (it's the base URL)
|
|
expect(url.searchParams.has(WebComponentProperty.ROOM_URL)).toBe(false);
|
|
expect(url.searchParams.has(WebComponentProperty.RECORDING_URL)).toBe(false);
|
|
});
|
|
|
|
it('should preserve existing query parameters in room-url', () => {
|
|
const roomUrl = 'https://example.com/room/testRoom?secret=abc123&role=moderator';
|
|
const participantName = 'Alice';
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// Original query params should be preserved
|
|
expect(url.searchParams.get('secret')).toBe('abc123');
|
|
expect(url.searchParams.get('role')).toBe('moderator');
|
|
// New param should be added
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// CUSTOM/UNKNOWN ATTRIBUTES
|
|
// ==========================================
|
|
describe('Custom Attributes as Query Parameters', () => {
|
|
it('should add custom attributes as query parameters', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute('custom-attr', 'custom-value');
|
|
component.setAttribute('another-param', 'another-value');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
expect(url.searchParams.get('custom-attr')).toBe('custom-value');
|
|
expect(url.searchParams.get('another-param')).toBe('another-value');
|
|
});
|
|
|
|
it('should handle attribute names with special characters', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute('data-test-id', '12345');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
expect(url.searchParams.get('data-test-id')).toBe('12345');
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// EDGE CASES
|
|
// ==========================================
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty string attributes', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, '');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// Empty string should still be added as query param
|
|
expect(url.searchParams.has(WebComponentProperty.PARTICIPANT_NAME)).toBe(true);
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('');
|
|
});
|
|
|
|
it('should handle special characters in attribute values', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
const specialName = 'User Name With Spaces & Special=Chars';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, specialName);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// Should be URL-encoded properly
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(specialName);
|
|
});
|
|
|
|
it('should handle updating attributes after initial render', () => {
|
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
|
(component as any).updateIframeSrc();
|
|
|
|
const initialSrc = component.shadowRoot?.querySelector('iframe')?.src;
|
|
|
|
// Update an attribute
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, 'Updated Name');
|
|
(component as any).updateIframeSrc();
|
|
|
|
const updatedSrc = component.shadowRoot?.querySelector('iframe')?.src;
|
|
|
|
expect(initialSrc).not.toBe(updatedSrc);
|
|
|
|
const url = new URL(updatedSrc || '');
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('Updated Name');
|
|
});
|
|
|
|
it('should handle invalid URL gracefully', () => {
|
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Set an invalid URL
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, 'not-a-valid-url');
|
|
|
|
// Call updateIframeSrc directly - it should catch the error and log it
|
|
(component as any).updateIframeSrc();
|
|
|
|
// Verify error was logged with the invalid URL
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('Invalid URL provided: not-a-valid-url', expect.anything());
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// INTEGRATION TESTS
|
|
// ==========================================
|
|
describe('Integration Tests', () => {
|
|
it('should handle complete real-world scenario with room-url and multiple attributes', () => {
|
|
const roomUrl = 'https://meet.example.com/room/team-standup?secret=xyz789';
|
|
const participantName = 'John Doe';
|
|
const e2eeKey = 'my-secure-key';
|
|
const redirectUrl = 'https://example.com/dashboard';
|
|
|
|
component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
|
component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
|
|
component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// Verify base URL
|
|
expect(url.origin).toBe('https://meet.example.com');
|
|
expect(url.pathname).toBe('/room/team-standup');
|
|
|
|
// Verify all query parameters
|
|
expect(url.searchParams.get('secret')).toBe('xyz789');
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
|
expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
|
|
expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
|
|
|
|
// Verify origin was set correctly
|
|
expect((component as any).targetIframeOrigin).toBe('https://meet.example.com');
|
|
});
|
|
|
|
it('should handle complete real-world scenario with recording-url', () => {
|
|
const recordingUrl = 'https://recordings.example.com/view/rec-20231115-abc123';
|
|
const participantName = 'Viewer';
|
|
|
|
component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
|
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
|
component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
|
|
|
|
(component as any).updateIframeSrc();
|
|
|
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
|
const url = new URL(iframe?.src || '');
|
|
|
|
// Verify base URL
|
|
expect(url.origin).toBe('https://recordings.example.com');
|
|
expect(url.pathname).toBe('/view/rec-20231115-abc123');
|
|
|
|
// Verify query parameters
|
|
expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
|
|
expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
|
|
|
|
// Verify origin was set correctly
|
|
expect((component as any).targetIframeOrigin).toBe('https://recordings.example.com');
|
|
});
|
|
});
|
|
});
|