ref: Extract test configuration code to TestsConfig.ts (#16329)

* ref: Move iFrameUsesJaas to TestsConfig.

* ref: Move room name prefix/suffix to config.

* ref: Move JaaS configuration to TestsConfig.

* ref: Move iframe config to TestsConfig.

* ref: Move webproxy config to TestsConfig.

* ref: Move JWT config to TestsConfig.

* doc: Document some of the IContext fields.

* Add a debug config option.
This commit is contained in:
bgrozev
2025-08-18 13:32:41 -05:00
committed by GitHub
parent fda42e5230
commit 91e4ac1665
16 changed files with 132 additions and 78 deletions

View File

@@ -34,6 +34,9 @@
# The JaaS tenant, e.g. vpaas-magic-cookie-abcdabcd1234567890
#JAAS_TENANT=
# Enable debug logging. Note this includes private information from .env.
#JITSI_DEBUG=true
# An access token to use to create meetings (used for the first participant)
#JWT_ACCESS_TOKEN=

View File

@@ -27,6 +27,7 @@ import Toolbar from '../pageobjects/Toolbar';
import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import Visitors from '../pageobjects/Visitors';
import { config as testsConfig } from './TestsConfig';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IToken } from './token';
import { IParticipantJoinOptions, IParticipantOptions } from './types';
@@ -225,8 +226,8 @@ export class Participant {
// @ts-ignore
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${options.roomName}"`;
if (process.env.IFRAME_TENANT) {
url = `${url}&tenant="${process.env.IFRAME_TENANT}"`;
if (testsConfig.iframe.tenant) {
url = `${url}&tenant="${testsConfig.iframe.tenant}"`;
} else if (baseUrl.pathname.length > 1) {
// remove leading slash
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;

View File

@@ -0,0 +1,44 @@
/**
* Global configuration that the tests are run with. Loaded from environment variables.
*/
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
iframe: {
customerId: process.env.IFRAME_TENANT?.trim()?.replace('vpaas-magic-cookie-', ''),
tenant: process.env.IFRAME_TENANT?.trim(),
/** Whether the configuration specifies a JaaS account for the iFrame API tests. */
usesJaas: Boolean(process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')),
},
jaas: {
/** Whether the configuration for JaaS specific tests is enabled. */
enabled: Boolean(process.env.JAAS_TENANT && process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID),
/** The JaaS key ID, used to sign the tokens. */
kid: process.env.JAAS_KID?.trim(),
/** The path to the JaaS private key, used to sign JaaS tokens. */
privateKeyPath: process.env.JAAS_PRIVATE_KEY_PATH?.trim(),
/** The JaaS tenant (vpaas-magic-cookie-<ID>) . */
tenant: process.env.JAAS_TENANT?.trim(),
},
jwt: {
kid: process.env.JWT_KID?.trim(),
/** A pre-configured token used by some tests. */
preconfiguredToken: process.env.JWT_ACCESS_TOKEN?.trim(),
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH?.trim()
},
roomName: {
/** Optional prefix for room names used for tests. */
prefix: process.env.ROOM_NAME_PREFIX?.trim(),
/** Optional suffix for room names used for tests. */
suffix: process.env.ROOM_NAME_SUFFIX?.trim()
},
webhooksProxy: {
enabled: Boolean(process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET),
sharedSecret: process.env.WEBHOOKS_PROXY_SHARED_SECRET?.trim(),
url: process.env.WEBHOOKS_PROXY_URL?.trim(),
}
};
if (config.debug) {
console.log('TestsConfig:', JSON.stringify(config, null, 2));
}

View File

@@ -1,6 +1,5 @@
import process from 'node:process';
import { P1, P2, P3, P4, Participant } from './Participant';
import { config } from './TestsConfig';
import { generateToken } from './token';
import { IJoinOptions, IParticipantOptions } from './types';
@@ -17,14 +16,14 @@ export async function ensureOneParticipant(options?: IJoinOptions): Promise<void
const participantOps = { name: P1 } as IParticipantOptions;
if (!options?.skipFirstModerator) {
const jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
const jwtPrivateKeyPath = config.jwt.privateKeyPath;
// 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
if (config.jwt.preconfiguredToken
&& ((jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|| !jwtPrivateKeyPath)) {
participantOps.token = { jwt: process.env.JWT_ACCESS_TOKEN };
participantOps.token = { jwt: config.jwt.preconfiguredToken };
} else if (jwtPrivateKeyPath) {
participantOps.token = generateToken({
...options?.tokenOptions,
@@ -209,8 +208,8 @@ export async function joinParticipant( // eslint-disable-line max-params
let forceTenant = options?.forceTenant;
if (options?.preferGenerateToken && !ctx.testProperties.useIFrameApi
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-') && process.env.IFRAME_TENANT) {
forceTenant = process.env.IFRAME_TENANT;
&& config.iframe.usesJaas && config.iframe.tenant) {
forceTenant = config.iframe.tenant;
}
await newParticipant.joinConference({
...options,

View File

@@ -2,6 +2,8 @@ import fs from 'fs';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { config } from './TestsConfig';
export type ITokenOptions = {
displayName?: string;
/**
@@ -10,12 +12,12 @@ export type ITokenOptions = {
exp?: string;
/**
* The key ID to use for the token.
* If not provided, JWT_KID will be used from the environment variables.
* If not provided, the kid configured with environment variables will be used (see env.example).
*/
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.
* If not provided, the path configured with environment variables will be used (see env.example).
*/
keyPath?: string;
/**
@@ -91,8 +93,8 @@ export function generatePayload(options: ITokenOptions): any {
* 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 keyId = options.keyId || config.jwt.kid;
const keyPath = options.keyPath || config.jwt.privateKeyPath;
const headers = {
algorithm: 'RS256',
noTimestamp: true,
@@ -101,11 +103,11 @@ export function generateToken(options: ITokenOptions): IToken {
};
if (!keyId) {
throw new Error('JWT_KID is not set');
throw new Error('No keyId provided (JWT_KID is not set?)');
}
if (!keyPath) {
throw new Error('JWT_PRIVATE_KEY_PATH is not set');
throw new Error('No keyPath provided (JWT_PRIVATE_KEY_PATH is not set?)');
}
const key = fs.readFileSync(keyPath);

View File

@@ -7,17 +7,26 @@ import { IToken, ITokenOptions } from './token';
export type IContext = {
/**
* Whether the configuration specifies a JaaS account for the iFrame API tests.
*/
iFrameUsesJaas: boolean;
* The up-to-four browser instances provided by the framework. These can be initialized using
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
**/
p1: Participant;
p2: Participant;
p3: Participant;
p4: Participant;
/** A room name automatically generated by the framework for convenience. */
roomName: string;
/**
* A flag that tests can set, which signals to the framework that the (rest of the) test suite should be skipped.
*/
skipSuiteTests: boolean;
/**
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
* set up the context appropriately.
**/
testProperties: ITestProperties;
times: any;
/** A WebhooksProxy instance generated by the framework and available for tests to use, if configured. */
webhooksProxy: WebhookProxy;
};

View File

@@ -1,16 +1,20 @@
import { config as testsConfig } from './TestsConfig';
const https = require('https');
/**
* Generates a random number between 1 and the specified maximum value (inclusive).
*
* @param {number} max - The maximum value for the random number (must be a positive integer).
* @param numberOfDigits - The number of digits to pad the random number with leading zeros.
* @return {string} The random number formatted with leading zeros if needed.
*/
export function getRandomNumberAsStr(max: number, numberOfDigits: number): string {
const randomNumber = Math.floor(Math.random() * max) + 1;
export function generateRoomName(testName: string) {
// XXX why chose between 1 and 40 and then always pad with an extra 0?
const rand = (Math.floor(Math.random() * 40) + 1).toString().padStart(3, '0');
let roomName = `${testName}-${rand}`;
return randomNumber.toString().padStart(numberOfDigits, '0');
if (testsConfig.roomName.prefix) {
roomName = `${testsConfig.roomName.prefix}_${roomName}`;
}
if (testsConfig.roomName.suffix) {
roomName += `_${testsConfig.roomName.suffix}`;
}
return roomName.toLowerCase();
}
/**

View File

@@ -1,12 +1,13 @@
import { multiremotebrowser } from '@wdio/globals';
import { config } from '../../helpers/TestsConfig';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('URL Normalisation', () => {
it('joining the meeting', async () => {
// if we are running with token this becomes ugly to match the URL
if (process.env.JWT_ACCESS_TOKEN) {
if (config.jwt.preconfiguredToken) {
ctx.skipSuiteTests = true;
return;

View File

@@ -1,4 +1,5 @@
import { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
@@ -78,7 +79,7 @@ describe('AVModeration', () => {
it('hangup and change moderator', async () => {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
if (config.iframe.usesJaas) {
return;
}

View File

@@ -1,4 +1,5 @@
import { P1, P3, Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants,
@@ -196,7 +197,7 @@ describe('Lobby', () => {
it('change of moderators in lobby', async () => {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
if (config.iframe.usesJaas) {
return;
}
await hangupAllParticipants();
@@ -288,7 +289,7 @@ describe('Lobby', () => {
it('moderator leaves while lobby enabled', async () => {
// no moderator switching if jaas is available.
if (ctx.iFrameUsesJaas) {
if (config.iframe.usesJaas) {
return;
}
const { p1, p2, p3 } = ctx;

View File

@@ -1,23 +1,24 @@
import type { Participant } from '../../helpers/Participant';
import { config } from '../../helpers/TestsConfig';
import { joinParticipant } from '../../helpers/participants';
import { IToken, ITokenOptions, generateToken } from '../../helpers/token';
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');
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
// 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,
keyId: options.keyId || config.jaas.kid,
keyPath: options.keyPath || config.jaas.privateKeyPath
});
}
/**
* Creates a new Participant and joins the MUC with the given name. The jaas-specific properties must be set as
* environment variables: IFRAME_TENANT.
* environment variables (see env.example and TestsConfig.ts).
*
* @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
@@ -28,15 +29,15 @@ export function generateJaasToken(options: ITokenOptions): IToken {
*/
export async function joinMuc(instanceId: 'p1' | 'p2' | 'p3' | 'p4', token?: IToken, roomName?: string):
Promise<Participant> {
if (!process.env.JAAS_TENANT) {
throw new Error('JAAS_TENANT environment variables must be set');
if (!config.jaas.enabled) {
throw new Error('JaaS is not configured.');
}
return await joinParticipant({
name: instanceId,
token
}, {
forceTenant: process.env.JAAS_TENANT,
forceTenant: config.jaas.tenant,
roomName
});
}

View File

@@ -1,5 +1,6 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import {
cleanup,
@@ -13,6 +14,8 @@ setTestProperties(__filename, {
useWebhookProxy: true
});
const customerId = testsConfig.iframe.customerId;
describe('Invite iframeAPI', () => {
let dialInDisabled: boolean;
let dialOutDisabled: boolean;
@@ -102,7 +105,6 @@ describe('Invite iframeAPI', () => {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const sipCallOutStartedEvent: {
customerId: string;
data: {
@@ -157,7 +159,6 @@ async function checkDialEvents(participant: Participant, direction: string, star
const { webhooksProxy } = ctx;
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const dialInStartedEvent: {
customerId: string;
data: {

View File

@@ -2,6 +2,7 @@ import { isEqual } from 'lodash-es';
import { P1, P2, Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
setTestProperties(__filename, {
@@ -45,7 +46,7 @@ async function checkParticipantLeftHook(p: Participant, reason: string, checkId
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
expect(event.customerId).toBe(process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', ''));
expect(event.customerId).toBe(testsConfig.iframe.customerId);
}
}
}

View File

@@ -1,4 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
setTestProperties(__filename, {
@@ -6,6 +7,8 @@ setTestProperties(__filename, {
useWebhookProxy: true
});
const { tenant, customerId } = testsConfig.iframe;
describe('Recording', () => {
let recordingDisabled: boolean;
let liveStreamingDisabled: boolean;
@@ -69,7 +72,6 @@ describe('Recording', () => {
});
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const liveStreamEvent: {
customerId: string;
eventType: string;
@@ -93,7 +95,6 @@ describe('Recording', () => {
await p1.getIframeAPI().executeCommand('stopRecording', 'stream');
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const liveStreamEvent: {
customerId: string;
eventType: string;
@@ -132,7 +133,6 @@ async function testRecordingStarted(command: boolean) {
}
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const recordingEvent: {
customerId: string;
eventType: string;
@@ -152,7 +152,7 @@ async function testRecordingStarted(command: boolean) {
const linkEvent = (await p1.getIframeAPI().getEventResult('recordingLinkAvailable'));
expect(linkEvent.link.startsWith('https://')).toBe(true);
expect(linkEvent.link.includes(process.env.IFRAME_TENANT)).toBe(true);
expect(linkEvent.link.includes(tenant)).toBe(true);
expect(linkEvent.ttl > 0).toBe(true);
}
@@ -171,7 +171,6 @@ async function testRecordingStopped(command: boolean) {
}
if (webhooksProxy) {
const customerId = process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', '');
const liveStreamEvent: {
customerId: string;
eventType: string;

View File

@@ -1,4 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
@@ -90,7 +91,7 @@ describe('Visitors', () => {
expect(event.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(event.data.name).toBe(p2.name);
expect(event.data.role).toBe('visitor');
expect(event.customerId).toBe(process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', ''));
expect(event.customerId).toBe(testsConfig.iframe.customerId);
await p2.switchToAPI();
await p2.getIframeAPI().executeCommand('hangup');
@@ -120,7 +121,7 @@ describe('Visitors', () => {
expect(eventLeft.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
expect(eventLeft.data.name).toBe(p2.name);
expect(eventLeft.data.role).toBe('visitor');
expect(eventLeft.customerId).toBe(process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', ''));
expect(eventLeft.customerId).toBe(testsConfig.iframe.customerId);
}
});
});

View File

@@ -7,10 +7,11 @@ import process from 'node:process';
import pretty from 'pretty';
import { getTestProperties, loadTestFiles } from './helpers/TestProperties';
import { config as testsConfig } from './helpers/TestsConfig';
import WebhookProxy from './helpers/WebhookProxy';
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
import { IContext } from './helpers/types';
import { getRandomNumberAsStr } from './helpers/utils';
import { generateRoomName } from './helpers/utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const allure = require('allure-commandline');
@@ -231,33 +232,19 @@ export const config: WebdriverIO.MultiremoteConfig = {
bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
}));
globalAny.ctx.roomName = `${testName}-${getRandomNumberAsStr(40, 3)}`;
if (process.env.ROOM_NAME_PREFIX) {
globalAny.ctx.roomName = `${process.env.ROOM_NAME_PREFIX.trim()}_${globalAny.ctx.roomName}`;
}
if (process.env.ROOM_NAME_SUFFIX) {
globalAny.ctx.roomName += `_${process.env.ROOM_NAME_SUFFIX.trim()}`;
}
globalAny.ctx.roomName = globalAny.ctx.roomName.toLowerCase();
globalAny.ctx.iFrameUsesJaas = process.env.JWT_PRIVATE_KEY_PATH
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-');
const isJaasConfigured = process.env.JAAS_TENANT && process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID;
globalAny.ctx.roomName = generateRoomName(testName);
// 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 (testProperties.useWebhookProxy) {
if (!globalAny.ctx.webhooksProxy
&& process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET) {
globalAny.ctx.webhooksProxy = new WebhookProxy(
`${process.env.WEBHOOKS_PROXY_URL}?tenant=${
isJaasConfigured ? process.env.JAAS_TENANT : process.env.IFRAME_TENANT
}&room=${globalAny.ctx.roomName}`,
process.env.WEBHOOKS_PROXY_SHARED_SECRET,
`${TEST_RESULTS_DIR}/webhooks-${cid}-${testName}.log`);
globalAny.ctx.webhooksProxy.connect();
}
if (testProperties.useWebhookProxy && testsConfig.webhooksProxy.enabled && !globalAny.ctx.webhooksProxy) {
// Note this prevents iframe and jaas test from running together.
const tenant = testsConfig.jaas.enabled ? testsConfig.jaas.tenant : testsConfig.iframe.tenant;
globalAny.ctx.webhooksProxy = new WebhookProxy(
`${testsConfig.webhooksProxy.url}?tenant=${tenant}&room=${globalAny.ctx.roomName}`,
testsConfig.webhooksProxy.sharedSecret!,
`${TEST_RESULTS_DIR}/webhooks-${cid}-${testName}.log`);
globalAny.ctx.webhooksProxy.connect();
}
if (testProperties.useWebhookProxy && !globalAny.ctx.webhooksProxy) {
@@ -265,9 +252,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
globalAny.ctx.skipSuiteTests = true;
}
if (testProperties.useJaas && !isJaasConfigured) {
console.warn(`JaaS is not configured, skipping ${testName}. `
+ 'Set JAAS_TENANT, JAAS_KID, and JAAS_PRIVATE_KEY_PATH to enable.');
if (testProperties.useJaas && !testsConfig.jaas.enabled) {
console.warn(`JaaS is not configured, skipping ${testName}.`);
globalAny.ctx.skipSuiteTests = true;
}
},