From 252ef4604affef88a0d4ff8c679d56a55c9cbdda Mon Sep 17 00:00:00 2001 From: bgrozev Date: Mon, 4 Aug 2025 04:28:38 -0500 Subject: [PATCH] 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. --- tests/env.example | 100 +++++++------ tests/helpers/Participant.ts | 38 +++-- tests/helpers/TestProperties.ts | 43 ++++++ tests/helpers/participants.ts | 86 +++--------- tests/helpers/token.ts | 132 ++++++++++++++++++ tests/helpers/types.ts | 18 +-- tests/specs/2way/iFrameApiChat.spec.ts | 5 + .../iFrameApiParticipantsPresence.spec.ts | 30 ++-- .../2way/iFrameApiTranscriptions.spec.ts | 6 + tests/specs/2way/iFrameApiVisitors.spec.ts | 10 +- .../specs/2way/iFrameApiVisitorsLive.spec.ts | 8 +- tests/specs/2way/lockRoom.spec.ts | 12 +- tests/specs/3way/audioVideoModeration.spec.ts | 4 +- tests/specs/3way/lobby.spec.ts | 8 +- tests/specs/alone/iFrameApiInvite.spec.ts | 6 + tests/specs/alone/iFrameApiRecording.spec.ts | 8 +- tests/specs/helpers/jaas.ts | 51 +++++++ tests/specs/jaas/joinMuc.spec.ts | 63 +++++++++ tests/specs/jaas/maxOccupants.spec.ts | 30 ++++ .../visitors/participantsSoftLimit.spec.ts | 52 +++++++ .../specs/jaas/visitors/visitorTokens.spec.ts | 61 ++++++++ tests/wdio.conf.ts | 29 +++- 22 files changed, 637 insertions(+), 163 deletions(-) create mode 100644 tests/helpers/TestProperties.ts create mode 100644 tests/helpers/token.ts create mode 100644 tests/specs/helpers/jaas.ts create mode 100644 tests/specs/jaas/joinMuc.spec.ts create mode 100644 tests/specs/jaas/maxOccupants.spec.ts create mode 100644 tests/specs/jaas/visitors/participantsSoftLimit.spec.ts create mode 100644 tests/specs/jaas/visitors/visitorTokens.spec.ts diff --git a/tests/env.example b/tests/env.example index 3d73c06ac3..38fb805c87 100644 --- a/tests/env.example +++ b/tests/env.example @@ -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= diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts index d8189889f4..493db31993 100644 --- a/tests/helpers/Participant.ts +++ b/tests/helpers/Participant.ts @@ -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; + } } diff --git a/tests/helpers/TestProperties.ts b/tests/helpers/TestProperties.ts new file mode 100644 index 0000000000..9341334577 --- /dev/null +++ b/tests/helpers/TestProperties.ts @@ -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 = {}; + +/** + * 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): 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 - The test properties with defaults applied + */ +export async function getTestProperties(testFilePath: string): Promise { + return testProperties[testFilePath] || { ...defaultProperties }; +} diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts index ae340af119..5b8136a47f 100644 --- a/tests/helpers/participants.ts +++ b/tests/helpers/participants.ts @@ -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. diff --git a/tests/helpers/token.ts b/tests/helpers/token.ts new file mode 100644 index 0000000000..1bbed43989 --- /dev/null +++ b/tests/helpers/token.ts @@ -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; +} diff --git a/tests/helpers/types.ts b/tests/helpers/types.ts index 58b6ef3521..146f31b307 100644 --- a/tests/helpers/types.ts +++ b/tests/helpers/types.ts @@ -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; @@ -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; }; diff --git a/tests/specs/2way/iFrameApiChat.spec.ts b/tests/specs/2way/iFrameApiChat.spec.ts index 8b103d030b..c2aa41d889 100644 --- a/tests/specs/2way/iFrameApiChat.spec.ts +++ b/tests/specs/2way/iFrameApiChat.spec.ts @@ -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 () => { diff --git a/tests/specs/2way/iFrameApiParticipantsPresence.spec.ts b/tests/specs/2way/iFrameApiParticipantsPresence.spec.ts index 428edabf74..134be75031 100644 --- a/tests/specs/2way/iFrameApiParticipantsPresence.spec.ts +++ b/tests/specs/2way/iFrameApiParticipantsPresence.spec.ts @@ -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 diff --git a/tests/specs/2way/iFrameApiTranscriptions.spec.ts b/tests/specs/2way/iFrameApiTranscriptions.spec.ts index 1f64105219..e7a1d39822 100644 --- a/tests/specs/2way/iFrameApiTranscriptions.spec.ts +++ b/tests/specs/2way/iFrameApiTranscriptions.spec.ts @@ -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); diff --git a/tests/specs/2way/iFrameApiVisitors.spec.ts b/tests/specs/2way/iFrameApiVisitors.spec.ts index 59e88556bd..69281bfdba 100644 --- a/tests/specs/2way/iFrameApiVisitors.spec.ts +++ b/tests/specs/2way/iFrameApiVisitors.spec.ts @@ -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); diff --git a/tests/specs/2way/iFrameApiVisitorsLive.spec.ts b/tests/specs/2way/iFrameApiVisitorsLive.spec.ts index 4185a7c550..392ea9f827 100644 --- a/tests/specs/2way/iFrameApiVisitorsLive.spec.ts +++ b/tests/specs/2way/iFrameApiVisitorsLive.spec.ts @@ -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 }); diff --git a/tests/specs/2way/lockRoom.spec.ts b/tests/specs/2way/lockRoom.spec.ts index 5584dd97ca..cd0a77c1f6 100644 --- a/tests/specs/2way/lockRoom.spec.ts +++ b/tests/specs/2way/lockRoom.spec.ts @@ -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(); diff --git a/tests/specs/3way/audioVideoModeration.spec.ts b/tests/specs/3way/audioVideoModeration.spec.ts index a3e0c0c297..0446f68f4e 100644 --- a/tests/specs/3way/audioVideoModeration.spec.ts +++ b/tests/specs/3way/audioVideoModeration.spec.ts @@ -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; } diff --git a/tests/specs/3way/lobby.spec.ts b/tests/specs/3way/lobby.spec.ts index 49e0db2394..2f9fe7a1b0 100644 --- a/tests/specs/3way/lobby.spec.ts +++ b/tests/specs/3way/lobby.spec.ts @@ -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; diff --git a/tests/specs/alone/iFrameApiInvite.spec.ts b/tests/specs/alone/iFrameApiInvite.spec.ts index a04215cbe1..8c743b245e 100644 --- a/tests/specs/alone/iFrameApiInvite.spec.ts +++ b/tests/specs/alone/iFrameApiInvite.spec.ts @@ -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); diff --git a/tests/specs/alone/iFrameApiRecording.spec.ts b/tests/specs/alone/iFrameApiRecording.spec.ts index 1c661c82bd..ad26bf67ef 100644 --- a/tests/specs/alone/iFrameApiRecording.spec.ts +++ b/tests/specs/alone/iFrameApiRecording.spec.ts @@ -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( diff --git a/tests/specs/helpers/jaas.ts b/tests/specs/helpers/jaas.ts new file mode 100644 index 0000000000..695a032f8b --- /dev/null +++ b/tests/specs/helpers/jaas.ts @@ -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, + }); +} diff --git a/tests/specs/jaas/joinMuc.spec.ts b/tests/specs/jaas/joinMuc.spec.ts new file mode 100644 index 0000000000..4b0d2c0438 --- /dev/null +++ b/tests/specs/jaas/joinMuc.spec.ts @@ -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); + // }); +}); diff --git a/tests/specs/jaas/maxOccupants.spec.ts b/tests/specs/jaas/maxOccupants.spec.ts new file mode 100644 index 0000000000..61795ffc0b --- /dev/null +++ b/tests/specs/jaas/maxOccupants.spec.ts @@ -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); + }); +}); diff --git a/tests/specs/jaas/visitors/participantsSoftLimit.spec.ts b/tests/specs/jaas/visitors/participantsSoftLimit.spec.ts new file mode 100644 index 0000000000..91b264c9fd --- /dev/null +++ b/tests/specs/jaas/visitors/participantsSoftLimit.spec.ts @@ -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'); + }); +}); diff --git a/tests/specs/jaas/visitors/visitorTokens.spec.ts b/tests/specs/jaas/visitors/visitorTokens.spec.ts new file mode 100644 index 0000000000..4b5fcbc0c6 --- /dev/null +++ b/tests/specs/jaas/visitors/visitorTokens.spec.ts @@ -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'); + }); +}); diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index 1791a07650..80c9d10ce0 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -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() {