This commit is contained in:
2026-05-25 22:58:11 +08:00
parent e6dfb28ef2
commit d74a0c8121
12 changed files with 347 additions and 189 deletions

View File

@@ -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);

View 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);
});
});

View File

@@ -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;