优化
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import { MeetingRecorder } from '../../public/call/media/meeting-recorder.js';
|
||||
|
||||
class MediaStreamMock {
|
||||
@@ -107,7 +108,7 @@ describe('MeetingRecorder', () => {
|
||||
const result = await recorder.stop();
|
||||
|
||||
expect(result.filename).toContain('meeting-recording-123-456-789');
|
||||
expect(result.mimeType).toBe('video/mp4;codecs=avc1.42E01E,mp4a.40.2');
|
||||
expect(result.mimeType.toLowerCase()).toBe('video/mp4;codecs=avc1.42e01e,mp4a.40.2');
|
||||
expect(result.filename).toMatch(/\.mp4$/);
|
||||
expect(windowRef.URL.createObjectURL).not.toHaveBeenCalled();
|
||||
expect(recorder.isRecording()).toBe(false);
|
||||
|
||||
88
client/test/unit/rendering-safety.test.js
Normal file
88
client/test/unit/rendering-safety.test.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { createMessageElement } from '../../public/call/chat/renderer-chat.js';
|
||||
import { createParticipantTile, getParticipantTile } from '../../public/call/participants/renderer-participant-grid.js';
|
||||
import { createUserEntryElement } from '../../public/call/renderers/renderer-ui.js';
|
||||
import { renderOnlineUsers } from '../../public/call/signaling/connect-directory.js';
|
||||
|
||||
const formatTimestamp = value => value;
|
||||
const unsafeText = '<img src=x onerror=alert(1)>Alice';
|
||||
|
||||
function mediaState(overrides = {}) {
|
||||
return {
|
||||
audio: true,
|
||||
video: true,
|
||||
isSpeaking: false,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('safe dynamic rendering', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('renders chat text as text, not markup', () => {
|
||||
const element = createMessageElement({
|
||||
id: 'msg-1',
|
||||
type: 'text',
|
||||
isSelf: false,
|
||||
senderName: unsafeText,
|
||||
senderAvatar: '/images/p1.png',
|
||||
content: unsafeText,
|
||||
timestamp: 'now'
|
||||
}, formatTimestamp);
|
||||
|
||||
expect(element.querySelector('.message-text').textContent).toBe(unsafeText);
|
||||
expect(element.querySelector('.message-content img')).toBeNull();
|
||||
expect(element.querySelector('.message-sender').textContent).toBe(unsafeText);
|
||||
});
|
||||
|
||||
test('renders participant names safely and finds ids without selector injection', () => {
|
||||
const participantId = 'room"] [data-bad="1';
|
||||
const tile = createParticipantTile(participantId, unsafeText);
|
||||
const grid = document.createElement('div');
|
||||
grid.appendChild(tile);
|
||||
|
||||
expect(tile.querySelector('.absolute.bottom-3 span').textContent).toBe(unsafeText);
|
||||
expect(tile.querySelector('.absolute.bottom-3 img')).toBeNull();
|
||||
expect(getParticipantTile(grid, participantId)).toBe(tile);
|
||||
});
|
||||
|
||||
test('renders user list entries without interpreting user profile fields as HTML', () => {
|
||||
const entry = createUserEntryElement({
|
||||
role: 'participant',
|
||||
id: 'participant-1',
|
||||
user: {
|
||||
name: unsafeText,
|
||||
avatar: '/images/p2.png',
|
||||
mediaState: mediaState({ audio: false })
|
||||
}
|
||||
});
|
||||
|
||||
expect(entry.textContent).toContain(unsafeText);
|
||||
expect(entry.querySelectorAll('img')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('renders online users without injecting markup from directory data', () => {
|
||||
const onlineUsersList = document.createElement('div');
|
||||
const usersContainer = document.createElement('div');
|
||||
const onlineUsersSummary = document.createElement('div');
|
||||
|
||||
renderOnlineUsers({
|
||||
users: [{
|
||||
name: unsafeText,
|
||||
userId: unsafeText,
|
||||
avatar: '/images/p1.png',
|
||||
role: 'participant',
|
||||
connectionId: 'room-1'
|
||||
}],
|
||||
currentUserId: 'other-user',
|
||||
onlineUsersList,
|
||||
usersContainer,
|
||||
onlineUsersSummary
|
||||
});
|
||||
|
||||
expect(usersContainer.textContent).toContain(unsafeText);
|
||||
expect(usersContainer.querySelector('button')).toBeNull();
|
||||
expect(usersContainer.querySelectorAll('img')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,49 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import * as Path from 'path';
|
||||
import process from 'process';
|
||||
import { setup, teardown } from 'jest-dev-server';
|
||||
import { Signaling, WebSocketSignaling } from "../../src/core/signaling.js";
|
||||
import { MockSignaling, reset } from "../mocks/mocksignaling.js";
|
||||
import { waitFor, sleep, serverExeName } from "../helpers/testutils.js";
|
||||
|
||||
const portNumber = 8081;
|
||||
const runSignalingIntegration = process.env.RUN_SIGNALING_INTEGRATION === '1';
|
||||
const signalingModes = runSignalingIntegration
|
||||
? [{ mode: "mock" }, { mode: "http" }, { mode: "websocket" }]
|
||||
: [{ mode: "mock" }];
|
||||
|
||||
jest.setTimeout(10000);
|
||||
|
||||
describe.each([
|
||||
{ mode: "mock" },
|
||||
{ mode: "http" },
|
||||
{ mode: "websocket" },
|
||||
])('signaling test in public mode', ({ mode }) => {
|
||||
function buildServerCommand(args = '') {
|
||||
const binaryPath = Path.resolve(`../bin~/${serverExeName()}`);
|
||||
const buildEntryPath = Path.resolve('../build/index.js');
|
||||
const serverCommand = fs.existsSync(binaryPath)
|
||||
? `"${binaryPath}"`
|
||||
: `"${process.execPath}" "${buildEntryPath}"`;
|
||||
|
||||
return `${serverCommand} ${args}`.trim();
|
||||
}
|
||||
|
||||
function getPortForMode(mode, isPrivate) {
|
||||
if (mode === 'mock') {
|
||||
return portNumber;
|
||||
}
|
||||
|
||||
const publicPorts = { http: portNumber + 1, websocket: portNumber + 2 };
|
||||
const privatePorts = { http: portNumber + 3, websocket: portNumber + 4 };
|
||||
return (isPrivate ? privatePorts : publicPorts)[mode];
|
||||
}
|
||||
|
||||
function createHttpSignaling(port) {
|
||||
return new Signaling(1, `http://localhost:${port}`);
|
||||
}
|
||||
|
||||
function createWebSocketSignaling(port) {
|
||||
return new WebSocketSignaling(1, `ws://localhost:${port}`);
|
||||
}
|
||||
|
||||
describe.each(signalingModes)('signaling test in public mode', ({ mode }) => {
|
||||
let signaling1;
|
||||
let signaling2;
|
||||
const connectionId1 = "12345";
|
||||
@@ -26,22 +57,22 @@ describe.each([
|
||||
signaling1 = new MockSignaling(1);
|
||||
signaling2 = new MockSignaling(1);
|
||||
} else {
|
||||
const path = Path.resolve(`../bin~/${serverExeName()}`);
|
||||
let cmd = `${path} -p ${portNumber}`;
|
||||
const serverPort = getPortForMode(mode, false);
|
||||
let cmd = buildServerCommand(`-p ${serverPort}`);
|
||||
if (mode == "http") {
|
||||
cmd += " -t http";
|
||||
}
|
||||
|
||||
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
|
||||
await setup({ command: cmd, port: serverPort, usedPortAction: 'error' });
|
||||
|
||||
if (mode == "http") {
|
||||
signaling1 = new Signaling(1);
|
||||
signaling2 = new Signaling(1);
|
||||
signaling1 = createHttpSignaling(serverPort);
|
||||
signaling2 = createHttpSignaling(serverPort);
|
||||
}
|
||||
|
||||
if (mode == "websocket") {
|
||||
signaling1 = new WebSocketSignaling(1);
|
||||
signaling2 = new WebSocketSignaling(1);
|
||||
signaling1 = createWebSocketSignaling(serverPort);
|
||||
signaling2 = createWebSocketSignaling(serverPort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +81,8 @@ describe.each([
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await signaling1.stop();
|
||||
await signaling2.stop();
|
||||
await signaling1?.stop();
|
||||
await signaling2?.stop();
|
||||
signaling1 = null;
|
||||
signaling2 = null;
|
||||
|
||||
@@ -207,11 +238,7 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ mode: "mock" },
|
||||
{ mode: "http" },
|
||||
{ mode: "websocket" },
|
||||
])('signaling test in private mode', ({ mode }) => {
|
||||
describe.each(signalingModes)('signaling test in private mode', ({ mode }) => {
|
||||
let signaling1;
|
||||
let signaling2;
|
||||
const connectionId = "12345";
|
||||
@@ -226,22 +253,22 @@ describe.each([
|
||||
return;
|
||||
}
|
||||
|
||||
const path = Path.resolve(`../bin~/${serverExeName()}`);
|
||||
let cmd = `${path} -p ${portNumber} -m private`;
|
||||
const serverPort = getPortForMode(mode, true);
|
||||
let cmd = buildServerCommand(`-p ${serverPort} -m private`);
|
||||
if (mode == "http") {
|
||||
cmd += " -t http";
|
||||
}
|
||||
|
||||
await setup({ command: cmd, port: portNumber, usedPortAction: 'error' });
|
||||
await setup({ command: cmd, port: serverPort, usedPortAction: 'error' });
|
||||
|
||||
if (mode == "http") {
|
||||
signaling1 = new Signaling(1);
|
||||
signaling2 = new Signaling(1);
|
||||
signaling1 = createHttpSignaling(serverPort);
|
||||
signaling2 = createHttpSignaling(serverPort);
|
||||
}
|
||||
|
||||
if (mode == "websocket") {
|
||||
signaling1 = new WebSocketSignaling(1);
|
||||
signaling2 = new WebSocketSignaling(1);
|
||||
signaling1 = createWebSocketSignaling(serverPort);
|
||||
signaling2 = createWebSocketSignaling(serverPort);
|
||||
}
|
||||
|
||||
await signaling1.start();
|
||||
@@ -249,8 +276,8 @@ describe.each([
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await signaling1.stop();
|
||||
await signaling2.stop();
|
||||
await signaling1?.stop();
|
||||
await signaling2?.stop();
|
||||
signaling1 = null;
|
||||
signaling2 = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user