test: Add JaaS-specific tests: join MUC, visitors, maxOccupants. (#16270)

* test: Add tests for joining a JaaS MUC with different token options.
* ref: Refactor token generation and usage
* ref: Reduce usage of global context 
* test: Add a maxOccupants jaas test.
This commit is contained in:
bgrozev
2025-08-04 04:28:38 -05:00
committed by GitHub
parent fc816aa149
commit 252ef4604a
22 changed files with 637 additions and 163 deletions

View File

@@ -1,47 +1,13 @@
# Ignore certificate errors (self-signed certificates)
#ALLOW_INSECURE_CERTS=true
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
# If there is a tenant in the URL it must end with a slash (e.g. "https://alpha.jitsi.net/sometenant/")
#BASE_URL=
# Room name suffix to use when creating new room names
#ROOM_NAME_SUFFIX=
# Room name prefix to use when creating new room names
#ROOM_NAME_PREFIX=
# To be able to match a domain to a specific address
# The format is "MAP example.com 1.2.3.4"
#RESOLVER_RULES=
# Ignore certificate errors (self-signed certificates)
#ALLOW_INSECURE_CERTS=true
# Whether to run the browser in headless mode
#HEADLESS=false
# The path to the browser video capture file
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
#IFRAME_TENANT=
# The grid host url (https://mygrid.com/wd/hub)
#GRID_HOST_URL=
# The path to the private key used for generating JWT token (.pk)
#JWT_PRIVATE_KEY_PATH=
# The kid to use in the token
#JWT_KID=
# An access token to use to create meetings (used for the first participant)
#JWT_ACCESS_TOKEN=
# The count of workers that execute the tests in parallel
# MAX_INSTANCES=1
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
#WEBHOOKS_PROXY_URL=
# A shared secret to authenticate the webhook proxy connection
#WEBHOOKS_PROXY_SHARED_SECRET=
# Whether to use beta for the first participants
#BROWSER_CHROME_BETA=false
#BROWSER_FF_BETA=false
# A rest URL to be used by dial-in tests to invite jigasi to the conference
#DIAL_IN_REST_URL=
@@ -49,12 +15,60 @@
# A destination number to dialout, that auto answers and sends media
#DIAL_OUT_URL=
# The grid host url (https://mygrid.com/wd/hub)
#GRID_HOST_URL=
# Whether to run the browser in headless mode
#HEADLESS=false
# The tenant used when executing the iframeAPI tests, will override any tenant from BASE_URL if any
#IFRAME_TENANT=
# Configure properties for jaas-specific tests (specs/jaas). Note that some of the iFrame tests can also
# be used to test JaaS, they are configured separately via IFRAME_TENANT, JWT_KID, JWT_PRIVATE_KEY_PATH.
# Domain for the JaaS environment, e.g. stage.8x8.vc
JAAS_DOMAIN=
# The key ID
JAAS_KID=
# The path to the private key used for generating JWT token (.pk) for jaas-specific tests
JAAS_PRIVATE_KEY_PATH=
# The JaaS tenant, e.g. vpaas-magic-cookie-abcdabcd1234567890
JAAS_TENANT=
# An access token to use to create meetings (used for the first participant)
#JWT_ACCESS_TOKEN=
# The kid to use in the token for non-jaas-specific tests (though it could be a jaas key).
#JWT_KID=
# The path to the private key used for generating JWT token (.pk) for non-jaas-specific tests (though it could be a
# jaas key).
#JWT_PRIVATE_KEY_PATH=
# The count of workers that execute the tests in parallel
# MAX_INSTANCES=1
# To be able to match a domain to a specific address
# The format is "MAP example.com 1.2.3.4"
#RESOLVER_RULES=
# Room name prefix to use when creating new room names
#ROOM_NAME_PREFIX=
# Room name suffix to use when creating new room names
#ROOM_NAME_SUFFIX=
# A destination number to dialout, that auto answer and sends media audio and video
#SIP_JIBRI_DIAL_OUT_URL=
# Whether to use beta for the first participants
#BROWSER_CHROME_BETA=false
#BROWSER_FF_BETA=false
# The path to the browser video capture file
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
# A shared secret to authenticate the webhook proxy connection
#WEBHOOKS_PROXY_SHARED_SECRET=
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
#WEBHOOKS_PROXY_URL=
# A stream key abd broadcast ID that can be used by the tests to stream to YouTube
#YTUBE_TEST_STREAM_KEY=

View File

@@ -24,6 +24,7 @@ import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import Visitors from '../pageobjects/Visitors';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IToken } from './token';
import { IContext, IJoinOptions } from './types';
export const P1 = 'p1';
@@ -49,7 +50,10 @@ export class Participant {
*/
private _name: string;
private _endpointId: string;
private _jwt?: string;
/**
* The token that this participant was initialized with.
*/
private _token?: IToken;
/**
* The default config to use when joining.
@@ -110,11 +114,11 @@ export class Participant {
* Creates a participant with given name.
*
* @param {string} name - The name of the participant.
* @param {string }jwt - The jwt if any.
* @param {string} token - The token if any.
*/
constructor(name: string, jwt?: string) {
constructor(name: string, token?: IToken) {
this._name = name;
this._jwt = jwt;
this._token = token;
}
/**
@@ -200,13 +204,13 @@ export class Participant {
};
}
if (ctx.iframeAPI) {
if (ctx.testProperties.useIFrameApi) {
config.room = 'iframeAPITest.html';
}
let url = urlObjectToString(config) || '';
if (ctx.iframeAPI) {
if (ctx.testProperties.useIFrameApi) {
const baseUrl = new URL(this.driver.options.baseUrl || '');
// @ts-ignore
@@ -219,8 +223,8 @@ export class Participant {
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
}
}
if (this._jwt) {
url = `${url}&jwt="${this._jwt}"`;
if (this._token?.jwt) {
url = `${url}&jwt="${this._token.jwt}"`;
}
if (options.baseUrl) {
@@ -231,7 +235,8 @@ export class Participant {
let urlToLoad = url.startsWith('/') ? url.substring(1) : url;
if (options.preferGenerateToken && !ctx.iframeAPI && ctx.isJaasAvailable() && process.env.IFRAME_TENANT) {
if (options.preferGenerateToken && !ctx.testProperties.useIFrameApi
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-') && process.env.IFRAME_TENANT) {
// This to enables tests like invite, which can force using the jaas auth instead of the provided token
urlToLoad = `/${process.env.IFRAME_TENANT}/${urlToLoad}`;
}
@@ -241,7 +246,7 @@ export class Participant {
await this.waitForPageToLoad();
if (ctx.iframeAPI) {
if (ctx.testProperties.useIFrameApi) {
const mainFrame = this.driver.$('iframe');
await this.driver.switchFrame(mainFrame);
@@ -338,6 +343,10 @@ export class Participant {
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
async isVisitor() {
return await this.execute(() => APP?.store?.getState()['features/visitors']?.iAmVisitor || false);
}
/**
* Checks if the meeting supports breakout rooms.
*/
@@ -447,7 +456,7 @@ export class Participant {
}
/**
* Waits for number of participants.
* Waits until the number of participants is exactly the given number.
*
* @param {number} number - The number of participant to wait for.
* @param {string} msg - A custom message to use.
@@ -891,4 +900,11 @@ export class Participant {
return this.driver.$(`//span[@id="participant_${endpointId}" and contains(@class, "dominant-speaker")]`)
.waitForDisplayed({ timeout: 5_000 });
}
/**
* Returns the token that this participant was initialized with.
*/
getToken(): IToken | undefined {
return this._token;
}
}

View File

@@ -0,0 +1,43 @@
/**
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
*/
export type ITestProperties = {
/** The test uses the iFrame API. */
useIFrameApi: boolean;
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
useJaas: boolean;
/** The test requires the webhook proxy. */
useWebhookProxy: boolean;
};
const defaultProperties: ITestProperties = {
useIFrameApi: false,
useWebhookProxy: false,
useJaas: false
};
const testProperties: Record<string, ITestProperties> = {};
/**
* Set properties for a test file. This was needed because I couldn't find a hook that executes with describe() before
* the code in wdio.conf.ts's before() hook. The intention is for tests to execute this directly. The properties don't
* change dynamically.
*
* @param filename the absolute path to the test file
* @param properties the properties to set for the test file, defaults will be applied for missing properties
*/
export function setTestProperties(filename: string, properties: Partial<ITestProperties>): void {
if (testProperties[filename]) {
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
}
testProperties[filename] = { ...defaultProperties, ...properties };
}
/**
* @param testFilePath - The absolute path to the test file
* @returns Promise<ITestProperties> - The test properties with defaults applied
*/
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
return testProperties[testFilePath] || { ...defaultProperties };
}

View File

@@ -1,9 +1,7 @@
import fs from 'fs';
import jwt from 'jsonwebtoken';
import process from 'node:process';
import { v4 as uuidv4 } from 'uuid';
import { P1, P2, P3, P4, Participant } from './Participant';
import { IToken, generateToken } from './token';
import { IContext, IJoinOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
@@ -171,7 +169,7 @@ async function _joinParticipant( // eslint-disable-line max-params
const p = ctx[name] as Participant;
if (p) {
if (ctx.iframeAPI) {
if (ctx.testProperties.useIFrameApi) {
await p.switchInPage();
}
@@ -179,7 +177,7 @@ async function _joinParticipant( // eslint-disable-line max-params
return;
}
if (ctx.iframeAPI) {
if (ctx.testProperties.useIFrameApi) {
// when loading url make sure we are on the top page context or strange errors may occur
await p.switchToAPI();
}
@@ -190,25 +188,33 @@ async function _joinParticipant( // eslint-disable-line max-params
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
}
let jwtToken;
let token: IToken = { jwt: '' };
if (name === P1) {
if (!options?.skipFirstModerator) {
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (process.env.JWT_ACCESS_TOKEN
&& ((ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.preferGenerateToken)
&& ((ctx.jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|| !ctx.jwtPrivateKeyPath)) {
jwtToken = process.env.JWT_ACCESS_TOKEN;
token = { jwt: process.env.JWT_ACCESS_TOKEN };
} else if (ctx.jwtPrivateKeyPath) {
jwtToken = getToken(ctx, name, options);
token = generateToken({
...options?.tokenOptions,
displayName: name,
});
}
}
} else if (name === P2) {
jwtToken = options?.preferGenerateToken ? getToken(ctx, P2, options) : undefined;
if (options?.preferGenerateToken) {
token = generateToken({
...options?.tokenOptions,
displayName: name,
});
}
}
const newParticipant = new Participant(name, jwtToken);
const newParticipant = new Participant(name, token);
// set the new participant instance
// @ts-ignore
@@ -283,64 +289,6 @@ export async function muteVideoAndCheck(testee: Participant, observer: Participa
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
}
/**
* Get a JWT token for a moderator.
*/
function getToken(ctx: IContext, displayName: string, options?: IJoinOptions) {
const keyid = process.env.JWT_KID;
const headers = {
algorithm: 'RS256',
noTimestamp: true,
expiresIn: '24h',
keyid
};
if (!keyid) {
console.error('JWT_KID is not set');
return;
}
const key = fs.readFileSync(ctx.jwtPrivateKeyPath);
const payload = {
'aud': 'jitsi',
'iss': 'chat',
'sub': keyid.substring(0, keyid.indexOf('/')),
'context': {
'user': {
'name': displayName,
'id': uuidv4(),
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
'email': 'john.doe@jitsi.org'
},
'group': uuidv4(),
'features': {
'outbound-call': 'true',
'transcription': 'true',
'recording': 'true',
'sip-outbound-call': true,
'livestreaming': true
},
},
'room': '*'
};
// if the moderator is set, or options are missing, we assume moderator
if (options?.moderator || !options) {
// @ts-ignore
payload.context.user.moderator = true;
} else if (options.visitor) {
// @ts-ignore
payload.context.user.role = 'visitor';
}
ctx.data[`${displayName}-jwt-payload`] = payload;
// @ts-ignore
return jwt.sign(payload, key, headers);
}
/**
* Parse a JID string.
* @param str the string to parse.

132
tests/helpers/token.ts Normal file
View File

@@ -0,0 +1,132 @@
import fs from 'fs';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
export type ITokenOptions = {
displayName?: string;
/**
* The duration for which the token is valid, e.g. "1h" for one hour.
*/
exp?: string;
/**
* The key ID to use for the token.
* If not provided, JWT_KID will be used from the environment variables.
*/
keyId?: string;
/**
* The path to the private key file used to sign the token.
* If not provided, JWT_PRIVATE_KEY_PATH will be used from the environment variables.
*/
keyPath?: string;
/**
* Whether to set the 'moderator' flag.
*/
moderator?: boolean;
/**
* The room for which the token is valid, or '*'. Defaults to '*'.
*/
room?: string;
sub?: string;
/**
* Whether to set the 'visitor' flag.
*/
visitor?: boolean;
};
export type IToken = {
/**
* The JWT headers, for easy reference.
*/
headers?: any;
/**
* The signed JWT.
*/
jwt: string;
/**
* The options used to generate the token.
*/
options?: ITokenOptions;
/**
* The token's payload, for easy reference.
*/
payload?: any;
};
export function generatePayload(options: ITokenOptions): any {
const payload = {
'aud': 'jitsi',
'iss': 'chat',
'sub': options?.sub || '',
'context': {
'user': {
'name': options.displayName,
'id': uuidv4(),
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
'email': 'john.doe@jitsi.org'
},
'group': uuidv4(),
'features': {
'outbound-call': 'true',
'transcription': 'true',
'recording': 'true',
'sip-outbound-call': true,
'livestreaming': true
},
},
'room': options.room || '*'
};
if (options.moderator) {
// @ts-ignore
payload.context.user.moderator = true;
} else if (options.visitor) {
// @ts-ignore
payload.context.user.role = 'visitor';
}
return payload;
}
/**
* Generate a signed token.
*/
export function generateToken(options: ITokenOptions): IToken {
const keyId = options.keyId || process.env.JWT_KID;
const keyPath = options.keyPath || process.env.JWT_PRIVATE_KEY_PATH;
const headers = {
algorithm: 'RS256',
noTimestamp: true,
expiresIn: options.exp || '24h',
keyid: keyId,
};
if (!keyId) {
throw new Error('JWT_KID is not set');
}
if (!keyPath) {
throw new Error('JWT_PRIVATE_KEY_PATH is not set');
}
const key = fs.readFileSync(keyPath);
const payload = generatePayload({
...options,
displayName: options?.displayName || '',
sub: keyId.substring(0, keyId.indexOf('/'))
});
return {
headers,
// @ts-ignore
jwt: jwt.sign(payload, key, headers),
options,
payload
};
}
/**
* Generated a signed token and return just the JWT string.
*/
export function generateJwt(options: ITokenOptions): string {
return generateToken(options).jwt;
}

View File

@@ -1,12 +1,16 @@
import { IConfig } from '../../react/features/base/config/configType';
import type { Participant } from './Participant';
import { ITestProperties } from './TestProperties';
import type WebhookProxy from './WebhookProxy';
import { ITokenOptions } from './token';
export type IContext = {
data: any;
iframeAPI: boolean;
isJaasAvailable: () => boolean;
/**
* Whether the configuration specifies a JaaS account for the iFrame API tests.
*/
iFrameUsesJaas: boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
keepAlive: Array<any>;
@@ -16,6 +20,7 @@ export type IContext = {
p4: Participant;
roomName: string;
skipSuiteTests: boolean;
testProperties: ITestProperties;
times: any;
webhooksProxy: WebhookProxy;
};
@@ -37,11 +42,6 @@ export type IJoinOptions = {
*/
displayName?: string;
/**
* Whether to create a moderator token for joining.
*/
moderator?: boolean;
/**
* When joining the first participant and jwt singing material is available and a provided token
* is available, prefer generating a new token for the first participant.
@@ -70,7 +70,7 @@ export type IJoinOptions = {
skipWaitToJoin?: boolean;
/**
* Whether to create a visitor token for joining.
* Options used when generating a token.
*/
visitor?: boolean;
tokenOptions?: ITokenOptions;
};

View File

@@ -1,9 +1,14 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
import { fetchJson } from '../../helpers/utils';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Chat', () => {
it('joining the meeting', async () => {

View File

@@ -1,13 +1,19 @@
import { isEqual } from 'lodash-es';
import { P1, P2, Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
import { IContext } from '../../helpers/types';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
/**
* Tests PARTICIPANT_LEFT webhook.
*/
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string, checkId = false) {
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string, checkId = false, conferenceJid: string) {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
@@ -28,14 +34,14 @@ async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: s
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
expect('PARTICIPANT_LEFT').toBe(event.eventType);
expect(event.data.conference).toBe(ctx.data.conferenceJid);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.disconnectReason).toBe(reason);
expect(event.data.isBreakout).toBe(false);
expect(event.data.participantId).toBe(await p.getEndpointId());
expect(event.data.name).toBe(p.name);
if (checkId) {
const jwtPayload = ctx.data[`${p.name}-jwt-payload`];
const jwtPayload = p.getToken()?.payload;
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
@@ -45,6 +51,8 @@ async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: s
}
describe('Participants presence', () => {
let conferenceJid: string = '';
it('joining the meeting', async () => {
// ensure 2 participants one moderator and one guest, we will load both with iframeAPI
await ensureTwoParticipants(ctx);
@@ -113,7 +121,7 @@ describe('Participants presence', () => {
const { node, resource } = parseJid(roomsInfo.jid);
ctx.data.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
const p1EpId = await p1.getEndpointId();
@@ -135,7 +143,7 @@ describe('Participants presence', () => {
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
expect('ROOM_CREATED').toBe(event.eventType);
expect(event.data.conference).toBe(ctx.data.conferenceJid);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
}
}
@@ -231,7 +239,7 @@ describe('Participants presence', () => {
const roomsInfo = (await p1.getIframeAPI().getRoomsInfo()).rooms[0];
ctx.data.conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
conferenceJid = roomsInfo.jid.substring(0, roomsInfo.jid.indexOf('/'));
await p1.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().addEventListener('participantKickedOut');
@@ -248,7 +256,7 @@ describe('Participants presence', () => {
timeoutMsg: 'participantKickedOut event not received on p2 side'
});
await checkParticipantLeftHook(ctx, p2, 'kicked', true);
await checkParticipantLeftHook(ctx, p2, 'kicked', true, conferenceJid);
expect(eventP1).toBeDefined();
expect(eventP2).toBeDefined();
@@ -315,7 +323,7 @@ describe('Participants presence', () => {
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
expect('PARTICIPANT_JOINED').toBe(event.eventType);
expect(event.data.conference).toBe(ctx.data.conferenceJid);
expect(event.data.conference).toBe(conferenceJid);
expect(event.data.isBreakout).toBe(false);
expect(event.data.moderator).toBe(false);
expect(event.data.name).toBe(await p2.getLocalDisplayName());
@@ -385,7 +393,7 @@ describe('Participants presence', () => {
expect(eventConferenceLeftP2).toBeDefined();
expect(eventConferenceLeftP2.roomName).toBe(roomName);
await checkParticipantLeftHook(ctx, p2, 'left');
await checkParticipantLeftHook(ctx, p2, 'left', false, conferenceJid);
const eventReadyToCloseP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('readyToClose'), {
timeout: 2000,
@@ -396,7 +404,7 @@ describe('Participants presence', () => {
});
it('dispose conference', async () => {
const { data: { conferenceJid }, p1, roomName, webhooksProxy } = ctx;
const { p1, roomName, webhooksProxy } = ctx;
await p1.switchToAPI();
@@ -414,7 +422,7 @@ describe('Participants presence', () => {
expect(eventConferenceLeft).toBeDefined();
expect(eventConferenceLeft.roomName).toBe(roomName);
await checkParticipantLeftHook(ctx, p1, 'left', true);
await checkParticipantLeftHook(ctx, p1, 'left', true, conferenceJid);
if (webhooksProxy) {
// ROOM_DESTROYED webhook
// @ts-ignore

View File

@@ -1,9 +1,15 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import type WebhookProxy from '../../helpers/WebhookProxy';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Transcriptions', () => {
it('joining the meeting', async () => {
await ensureOneParticipant(ctx);

View File

@@ -1,5 +1,11 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Visitors', () => {
it('joining the meeting', async () => {
const { webhooksProxy } = ctx;
@@ -33,7 +39,7 @@ describe('Visitors', () => {
it('visitor joins', async () => {
await ensureTwoParticipants(ctx, {
preferGenerateToken: true,
visitor: true,
tokenOptions: { visitor: true },
skipInMeetingChecks: true
});
@@ -72,7 +78,7 @@ describe('Visitors', () => {
eventType: string;
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
const jwtPayload = ctx.data[`${p2.name}-jwt-payload`];
const jwtPayload = p2.getToken()?.payload;
expect('PARTICIPANT_JOINED').toBe(event.eventType);
expect(event.data.avatar).toBe(jwtPayload.context.user.avatar);

View File

@@ -1,7 +1,13 @@
import { expect } from '@wdio/globals';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Visitors', () => {
it('joining the meeting', async () => {
const { webhooksProxy } = ctx;
@@ -37,7 +43,7 @@ describe('Visitors', () => {
it('go live', async () => {
await ensureTwoParticipants(ctx, {
preferGenerateToken: true,
visitor: true,
tokenOptions: { visitor: true },
skipWaitToJoin: true,
skipInMeetingChecks: true
});

View File

@@ -1,6 +1,8 @@
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
import type SecurityDialog from '../../pageobjects/SecurityDialog';
let roomKey: string;
/**
* 1. Lock the room (make sure the image changes to locked)
* 2. Join with a second browser/tab
@@ -29,14 +31,14 @@ describe('Lock Room', () => {
const p2PasswordDialog = p2.getPasswordDialog();
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(`${ctx.data.roomKey}1234`);
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
// give sometime to the password prompt to disappear and send the password
await p2.driver.pause(500);
// wait for password prompt
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(ctx.data.roomKey);
await p2PasswordDialog.submitPassword(roomKey);
await p2.waitToJoinMUC();
@@ -106,7 +108,7 @@ describe('Lock Room', () => {
const p2PasswordDialog = p2.getPasswordDialog();
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(`${ctx.data.roomKey}1234`);
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
// give sometime to the password prompt to disappear and send the password
await p2.driver.pause(500);
@@ -132,7 +134,7 @@ describe('Lock Room', () => {
* Participant1 locks the room.
*/
async function participant1LockRoom() {
ctx.data.roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
const { p1 } = ctx;
const p1SecurityDialog = p1.getSecurityDialog();
@@ -142,7 +144,7 @@ async function participant1LockRoom() {
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.addPassword(ctx.data.roomKey);
await p1SecurityDialog.addPassword(roomKey);
await p1SecurityDialog.clickCloseButton();

View File

@@ -77,8 +77,8 @@ describe('AVModeration', () => {
});
it('hangup and change moderator', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
return;
}

View File

@@ -195,8 +195,8 @@ describe('Lobby', () => {
});
it('change of moderators in lobby', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
return;
}
await hangupAllParticipants();
@@ -287,8 +287,8 @@ describe('Lobby', () => {
});
it('moderator leaves while lobby enabled', async () => {
// no moderator switching if jaas is available
if (ctx.isJaasAvailable()) {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
return;
}
const { p1, p2, p3 } = ctx;

View File

@@ -1,4 +1,5 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureOneParticipant } from '../../helpers/participants';
import {
cleanup,
@@ -8,6 +9,11 @@ import {
waitForAudioFromDialInParticipant
} from '../helpers/DialIn';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Invite iframeAPI', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);

View File

@@ -1,5 +1,11 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureOneParticipant } from '../../helpers/participants';
setTestProperties(__filename, {
useIFrameApi: true,
useWebhookProxy: true
});
describe('Recording', () => {
it('join participant', async () => {
await ensureOneParticipant(ctx);
@@ -180,7 +186,7 @@ async function testRecordingStopped(command: boolean) {
eventType: string;
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED', 20000);
const jwtPayload = ctx.data[`${p1.name}-jwt-payload`];
const jwtPayload = p1.getToken()?.payload;
expect(recordingUploadedEvent.data.initiatorId).toBe(jwtPayload?.context?.user?.id);
expect(recordingUploadedEvent.data.participants.some(

View File

@@ -0,0 +1,51 @@
import { Participant } from '../../helpers/Participant';
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
/**
* Creates a new Participant and joins the MUC with the given name. The jaas-specific properties must be set as
* environment variables: JAAS_DOMAIN and JAAS_TENANT, JAAS_KID, JAAS_PRIVATE_KEY_PATH.
*
* @param roomName The name of the room to join, without the tenant.
* @param instanceId This is the "name" passed to the Participant, I think it's used to match against one of the
* pre-configured browser instances in wdio? It must be one of 'p1', 'p2', 'p3', or 'p4'. TODO: figure out how this
* should be used.
* @param token the token to use, if any.
*/
export async function joinMuc(roomName: string, instanceId: 'p1' | 'p2' | 'p3' | 'p4', token?: IToken) {
if (!process.env.JAAS_DOMAIN || !process.env.JAAS_TENANT) {
throw new Error('JAAS_DOMAIN and JAAS_TENANT environment variables must be set');
}
// TODO: this should re-use code from Participant (e.g. setting config).
let url = `https://${process.env.JAAS_DOMAIN}/${process.env.JAAS_TENANT}/${roomName}`;
if (token) {
url += `?jwt=${token.jwt}`;
}
url += '#config.prejoinConfig.enabled=false';
const newParticipant = new Participant(instanceId, token);
try {
await newParticipant.driver.setTimeout({ 'pageLoad': 30000 });
await newParticipant.driver.url(url);
await newParticipant.waitForPageToLoad();
await newParticipant.waitToJoinMUC();
} catch (error) {
}
return newParticipant;
}
export function generateJaasToken(options: ITokenOptions): IToken {
if (!process.env.JAAS_PRIVATE_KEY_PATH || !process.env.JAAS_KID) {
throw new Error('JAAS_PRIVATE_KEY_PATH and JAAS_KID environment variables must be set');
}
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
return generateToken({
...options,
keyId: options.keyId || process.env.JAAS_KID,
keyPath: options.keyPath || process.env.JAAS_PRIVATE_KEY_PATH,
});
}

View File

@@ -0,0 +1,63 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true
});
describe('XMPP login and MUC join test', () => {
it('with a valid token (wildcard room)', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ room: '*' }));
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
});
it('with a valid token (specific room)', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName }));
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
});
it('with a token with bad signature', async () => {
const token = t({ room: ctx.roomName });
token.jwt = token.jwt + 'badSignature';
const p = await joinMuc(ctx.roomName, 'p1', token);
expect(Boolean(await p.isInMuc())).toBe(false);
});
it('with an expired token', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ exp: '-1m' }));
expect(Boolean(await p.isInMuc())).toBe(false);
});
it('with a token using the wrong key ID', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ keyId: 'invalid-key-id' }));
expect(Boolean(await p.isInMuc())).toBe(false);
});
it('with a token for a different room', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName + 'different' }));
expect(Boolean(await p.isInMuc())).toBe(false);
});
it('with a moderator token', async () => {
const p = await joinMuc(ctx.roomName, 'p1', t({ moderator: true }));
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(true);
});
// it('without sending a conference-request', async () => {
// console.log('Joining a MUC without sending a conference-request');
// // TODO verify failure
// //expect(await joinMuc(ctx.roomName, 'p1', token)).toBe(true);
// });
});

View File

@@ -0,0 +1,30 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('MaxOccupants limit enforcement', () => {
it('test maxOccupants limit', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
maxOccupants: 2
};
const p1 = await joinMuc(ctx.roomName, 'p1', t({ room: ctx.roomName }));
const p2 = await joinMuc(ctx.roomName, 'p2', t({ room: ctx.roomName }));
expect(await p1.isInMuc()).toBe(true);
expect(await p2.isInMuc()).toBe(true);
// Third participant should be rejected (exceeding maxOccupants), even if it's a moderator
let p3 = await joinMuc(ctx.roomName, 'p3', t({ room: ctx.roomName, moderator: true }));
expect(Boolean(await p3.isInMuc())).toBe(false);
await p1.hangup();
p3 = await joinMuc(ctx.roomName, 'p3', t({ room: ctx.roomName }));
expect(await p3.isInMuc()).toBe(true);
});
});

View File

@@ -0,0 +1,52 @@
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('Visitors triggered by reaching participantsSoftLimit', () => {
it('test participantsSoftLimit', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
participantsSoftLimit: 2,
visitorsEnabled: true
};
/// XXX the "name" of the participant MUST match one of the "capabilities" defined in wdio. It's not a "participant", it's an instance configuration!
const m = await joinMuc(
ctx.roomName,
'p1',
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
);
expect(await m.isInMuc()).toBe(true);
expect(await m.isModerator()).toBe(true);
expect(await m.isVisitor()).toBe(false);
console.log('Moderator joined');
// Joining with a participant token before participantSoftLimit has been reached
const p = await joinMuc(
ctx.roomName,
'p2',
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
);
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
expect(await p.isVisitor()).toBe(false);
console.log('Participant joined');
// Joining with a participant token after participantSoftLimit has been reached
const v = await joinMuc(
ctx.roomName,
'p3',
t({ room: ctx.roomName, displayName: 'Visi Tor' })
);
expect(await v.isInMuc()).toBe(true);
expect(await v.isModerator()).toBe(false);
expect(await v.isVisitor()).toBe(true);
console.log('Visitor joined');
});
});

View File

@@ -0,0 +1,61 @@
import { setTestProperties } from '../../../helpers/TestProperties';
import { joinMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
});
describe('Visitors triggered by visitor tokens', () => {
it('test visitor tokens', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
visitorsEnabled: true
};
const m = await joinMuc(
ctx.roomName,
'p1',
t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
);
expect(await m.isInMuc()).toBe(true);
expect(await m.isModerator()).toBe(true);
expect(await m.isVisitor()).toBe(false);
console.log('Moderator joined');
// Joining with a participant token before any visitors
const p = await joinMuc(
ctx.roomName,
'p2',
t({ room: ctx.roomName, displayName: 'Parti Cipant' })
);
expect(await p.isInMuc()).toBe(true);
expect(await p.isModerator()).toBe(false);
expect(await p.isVisitor()).toBe(false);
console.log('Participant joined');
// Joining with a visitor token
const v = await joinMuc(
ctx.roomName,
'p3',
t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
);
expect(await v.isInMuc()).toBe(true);
expect(await v.isModerator()).toBe(false);
expect(await v.isVisitor()).toBe(true);
console.log('Visitor joined');
// Joining with a participant token after visitors...:mindblown:
const v2 = await joinMuc(
ctx.roomName,
'p4',
t({ room: ctx.roomName, displayName: 'Visi Tor 2' }));
expect(await v2.isInMuc()).toBe(true);
expect(await v2.isModerator()).toBe(false);
expect(await v2.isVisitor()).toBe(true);
console.log('Visitor2 joined');
});
});

View File

@@ -5,6 +5,7 @@ import path from 'node:path';
import process from 'node:process';
import pretty from 'pretty';
import { getTestProperties } from './helpers/TestProperties';
import WebhookProxy from './helpers/WebhookProxy';
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
import { IContext } from './helpers/types';
@@ -180,7 +181,11 @@ export const config: WebdriverIO.MultiremoteConfig = {
console.warn('We expect to run a single suite, but got more than one');
}
const testName = path.basename(specs[0]).replace('.spec.ts', '');
const testFilePath = specs[0].replace(/^file:\/\//, '');
const testName = path.relative('tests/specs', testFilePath)
.replace(/.spec.ts$/, '')
.replace(/\//g, '-');
const testProperties = await getTestProperties(testFilePath);
console.log(`Running test: ${testName} via worker: ${cid}`);
@@ -191,6 +196,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
times: {}
} as IContext;
globalAny.ctx.keepAlive = [];
globalAny.ctx.testProperties = testProperties;
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);
@@ -224,13 +230,12 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.roomName = globalAny.ctx.roomName.toLowerCase();
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
globalAny.ctx.jwtKid = process.env.JWT_KID;
globalAny.ctx.isJaasAvailable = () => globalAny.ctx.jwtKid?.startsWith('vpaas-magic-cookie-');
globalAny.ctx.iFrameUsesJaas = process.env.JWT_PRIVATE_KEY_PATH
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-');
// If we are running the iFrameApi tests, we need to mark it as such and if needed to create the proxy
// and connect to it.
if (testName.startsWith('iFrameApi')) {
globalAny.ctx.iframeAPI = true;
if (testProperties.useWebhookProxy) {
if (!globalAny.ctx.webhooksProxy
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
globalAny.ctx.webhooksProxy = new WebhookProxy(
@@ -240,6 +245,20 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.webhooksProxy.connect();
}
}
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
console.warn(`WebhookProxy is not available, skipping ${testName}`);
globalAny.ctx.skipSuiteTests = true;
}
const isJaasConfigured = process.env.JAAS_DOMAIN && process.env.JAAS_TENANT
&& process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID;
if (testProperties.useJaas && !isJaasConfigured) {
console.warn(`JaaS is not configured, skipping ${testName}. `
+ 'Set JAAS_DOMAIN, JAAS_TENANT, JAAS_KID, and JAAS_PRIVATE_KEY_PATH to enable.');
globalAny.ctx.skipSuiteTests = true;
}
},
after() {