Files
upstream-jitsi-meet/tests/specs/misc/timeTimer.spec.ts
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

126 lines
5.2 KiB
TypeScript

import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
const PILL_SELECTOR = '[data-testid="time-timer-pill"]';
// The shape mod_time_restricted broadcasts. A participant that could get this
// honoured would be able to put an arbitrary countdown on everyone's screen.
//
// The duration is deliberately unlike anything a deployment would enforce, so
// it identifies the forgery: a target running mod_time_restricted pushes its
// own (much shorter) limit on join, and these tests must tell the two apart
// rather than assume no timer is running at all.
const FORGED_DURATION = 1800;
const PAYLOAD = {
type: 'time_restricted',
durationSeconds: FORGED_DURATION,
elapsedSeconds: 0
};
// JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED. Spelled out because
// the enum is not reachable from the page context.
const NON_PARTICIPANT_MESSAGE_RECEIVED = 'conference.non_participant_message_received';
/**
* The duration of the timer currently running for this participant, or 0 when
* none is. Read from redux rather than the pill: the pill is additionally
* gated by the suppression window, and the duration is what tells a forged
* timer apart from a legitimate server-pushed one.
*
* @param {Participant} p - The participant.
* @returns {Promise<number>}
*/
function timerDuration(p: Participant): Promise<number> {
return p.execute(() => APP.store.getState()['features/time-timer'].durationSeconds);
}
/**
* Delivers a NON_PARTICIPANT_MESSAGE_RECEIVED straight into the conference's
* event emitter, which is the seam the time-timer middleware listens on. `id`
* is the sender's endpoint id — null only when lib-jitsi-meet could not
* attribute the stanza to an occupant, i.e. when the server itself sent it.
*
* @param {Participant} p - The participant to deliver to.
* @param {string | null} id - The sender endpoint id to report.
* @returns {Promise<void>}
*/
function injectNonParticipantMessage(p: Participant, id: string | null): Promise<void> {
return p.execute((eventName: string, senderId: string | null, json: any) => {
APP.conference._room.eventEmitter.emit(eventName, senderId, json);
}, NON_PARTICIPANT_MESSAGE_RECEIVED, id, PAYLOAD);
}
describe('Time timer server message', () => {
it('joining the meeting', async () => {
// The default test config disables the timer, so enable it explicitly.
// The suppression window is pinned so the pill assertion at the end is
// about the timer and not about a deployment's display policy.
await ensureTwoParticipants({
configOverwrite: {
timeTimer: {
enabled: true,
suppressForSeconds: 0
}
}
});
// No assertion on whether a timer is already running: a target with
// mod_time_restricted enabled pushes its limit to every occupant on
// join, and one without it leaves the timer idle. Both are fine — what
// the tests below check is that the forged duration is never adopted.
expect(await timerDuration(ctx.p1)).not.toBe(FORGED_DURATION);
});
it('ignores the payload sent as a json-message by another participant', async () => {
const { p1, p2 } = ctx;
// The realistic attack: p2 puts the exact payload into the room. The MUC
// stamps p2's occupant JID on it, so p1 sees it as p2's message and must
// not adopt the duration it carries.
await p2.execute(payload => {
APP.conference._room.room.sendMessage(JSON.stringify(payload), 'json-message');
}, PAYLOAD);
await p1.driver.pause(2000);
expect(await timerDuration(p1)).not.toBe(FORGED_DURATION);
});
it('ignores a non-participant message that carries a sender id', async () => {
const { p1 } = ctx;
// A sender we cannot resolve to a participant — a hidden occupant, or one
// whose presence has not been processed yet. It still has a nick, so the
// message is attributable to somebody and must not be trusted.
await injectNonParticipantMessage(p1, 'abc123');
await p1.driver.pause(2000);
expect(await timerDuration(p1)).not.toBe(FORGED_DURATION);
});
it('starts the timer for a message with no sender id', async () => {
const { p1 } = ctx;
// The server-originated case: sent from the bare room JID, so there is no
// resource to derive an id from. This is the only shape that is honoured —
// and it proves the two cases above are rejected on the sender id, not
// because the payload or the wiring is broken.
await injectNonParticipantMessage(p1, null);
await p1.driver.waitUntil(async () => await timerDuration(p1) === FORGED_DURATION, {
timeout: 3000,
timeoutMsg: 'a server-originated time_restricted message did not start the timer'
});
await expect(await p1.driver.$(PILL_SELECTOR)).toBeDisplayed();
});
});