89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|