From fcd913c05e7f93ca3fb84200bd00020677e697df Mon Sep 17 00:00:00 2001 From: Ammar Ansari Date: Wed, 5 Jun 2024 15:54:12 +0200 Subject: [PATCH] Create and Delete CF resource on the fly (#1060) * Create and Delete resource on the fly * function to create swml application resource * relay app create and delete on the fly * integrate API in video room test * cleanup e2e-realtime utilities * create and delete swml resource --- internal/e2e-js/fixtures.ts | 62 ++++- .../tests/callfabric/conversation.spec.ts | 11 +- .../e2e-js/tests/callfabric/relayApp.spec.ts | 46 ++-- internal/e2e-js/tests/callfabric/swml.spec.ts | 64 +++-- .../e2e-js/tests/callfabric/videoRoom.spec.ts | 12 +- internal/e2e-js/utils.ts | 233 ++++++++---------- internal/e2e-realtime-api/src/utils.ts | 6 - .../src/voiceRecord/withDialListeners.test.ts | 1 - 8 files changed, 248 insertions(+), 187 deletions(-) diff --git a/internal/e2e-js/fixtures.ts b/internal/e2e-js/fixtures.ts index 00bb57436..3ac0adc3e 100644 --- a/internal/e2e-js/fixtures.ts +++ b/internal/e2e-js/fixtures.ts @@ -1,20 +1,39 @@ import type { Video } from '@signalwire/js' -import { PageWithWsInspector, intercepWsTraffic, } from 'playwrigth-ws-inspector' +import { PageWithWsInspector, intercepWsTraffic } from 'playwrigth-ws-inspector' import { test as baseTest, expect, type Page } from '@playwright/test' -import { enablePageLogs } from './utils' +import { + CreateRelayAppResourceParams, + CreateSWMLAppResourceParams, + Resource, + createRelayAppResource, + createSWMLAppResource, + createVideoRoomResource, + deleteResource, + enablePageLogs, +} from './utils' type CustomPage = Page & { swNetworkDown: () => Promise swNetworkUp: () => Promise } type CustomFixture = { - createCustomPage(options: { name: string }): Promise> + createCustomPage(options: { + name: string + }): Promise> createCustomVanillaPage(options: { name: string }): Promise + resource: { + createVideoRoomResource: typeof createVideoRoomResource + createSWMLAppResource: typeof createSWMLAppResource + createRelayAppResource: typeof createRelayAppResource + resources: Resource[] + } } const test = baseTest.extend({ createCustomPage: async ({ context }, use) => { - const maker = async (options: { name: string }): Promise> => { + const maker = async (options: { + name: string + }): Promise> => { let page = await context.newPage() enablePageLogs(page, options.name) //@ts-ignore @@ -68,7 +87,6 @@ const test = baseTest.extend({ expect(row.rootEl).toBe(0) }) }, - createCustomVanillaPage: async ({ context }, use) => { const maker = async (options: { name: string }): Promise => { const page = await context.newPage() @@ -79,6 +97,40 @@ const test = baseTest.extend({ console.log('Cleaning up pages..') }, + resource: async ({}, use) => { + const resources: Resource[] = [] + + const resource = { + createVideoRoomResource: async (params?: string) => { + const data = await createVideoRoomResource(params) + resources.push(data) + return data + }, + createSWMLAppResource: async (params: CreateSWMLAppResourceParams) => { + const data = await createSWMLAppResource(params) + resources.push(data) + return data + }, + createRelayAppResource: async (params: CreateRelayAppResourceParams) => { + const data = await createRelayAppResource(params) + resources.push(data) + return data + }, + resources, + } + await use(resource) + + // Clean up resources after use + const deleteResources = resources.map(async (resource) => { + try { + await deleteResource(resource.id) + console.log('>> Resource deleted successfully:', resource.id) + } catch (error) { + console.error('>> Failed to delete resource:', resource.id, error) + } + }) + await Promise.allSettled(deleteResources) + }, }) export { test, expect, Page } diff --git a/internal/e2e-js/tests/callfabric/conversation.spec.ts b/internal/e2e-js/tests/callfabric/conversation.spec.ts index 5c03a79c6..0666bbdfd 100644 --- a/internal/e2e-js/tests/callfabric/conversation.spec.ts +++ b/internal/e2e-js/tests/callfabric/conversation.spec.ts @@ -1,10 +1,13 @@ +import { uuid } from '@signalwire/core' import { SignalWireContract } from '@signalwire/js' import { test, expect } from '../../fixtures' -import { SERVER_URL, createVideoRoom, createCFClient } from '../../utils' -import { uuid } from '@signalwire/core' +import { SERVER_URL, createCFClient } from '../../utils' test.describe('Conversation Room', () => { - test('send message in a room conversation', async ({ createCustomPage }) => { + test('send message in a room conversation', async ({ + createCustomPage, + resource, + }) => { const page = await createCustomPage({ name: '[page]' }) const page2 = await createCustomPage({ name: '[page2]', @@ -16,7 +19,7 @@ test.describe('Conversation Room', () => { await createCFClient(page2) const roomName = `e2e-js-convo-room_${uuid()}` - await createVideoRoom(roomName) + await resource.createVideoRoomResource(roomName) const firstMsgEvent = await page.evaluate( ({ roomName }) => { diff --git a/internal/e2e-js/tests/callfabric/relayApp.spec.ts b/internal/e2e-js/tests/callfabric/relayApp.spec.ts index 0afc148ef..84cfeee7c 100644 --- a/internal/e2e-js/tests/callfabric/relayApp.spec.ts +++ b/internal/e2e-js/tests/callfabric/relayApp.spec.ts @@ -1,3 +1,4 @@ +import { uuid } from '@signalwire/core' import { SignalWire } from '@signalwire/realtime-api' import { createCFClient, @@ -12,6 +13,7 @@ import { test, expect } from '../../fixtures' test.describe('CallFabric Relay Application', () => { test('should connect to the relay app and expect an audio playback', async ({ createCustomPage, + resource, }) => { const client = await SignalWire({ host: process.env.RELAY_HOST, @@ -22,8 +24,14 @@ test.describe('CallFabric Relay Application', () => { }, }) + const reference = `e2e-relay-app_${uuid()}` + await resource.createRelayAppResource({ + name: reference, + reference, + }) + await client.voice.listen({ - topics: ['cf-e2e-test-relay'], + topics: [reference], onCallReceived: async (call) => { try { console.log('Call received', call.id) @@ -50,8 +58,6 @@ test.describe('CallFabric Relay Application', () => { await createCFClient(page) - const resourceName = 'cf-e2e-test-relay' - await page.evaluate( async (options) => { return new Promise(async (resolve, _reject) => { @@ -59,7 +65,7 @@ test.describe('CallFabric Relay Application', () => { const client = window._client const call = await client.dial({ - to: `/public/${options.resourceName}`, + to: `/private/${options.reference}`, rootElement: document.getElementById('rootElement'), }) @@ -69,7 +75,7 @@ test.describe('CallFabric Relay Application', () => { resolve(call) }) }, - { resourceName } + { reference } ) const callPlayStarted = page.evaluate(async () => { @@ -126,6 +132,7 @@ test.describe('CallFabric Relay Application', () => { test('should connect to the relay app and expect a silence', async ({ createCustomPage, + resource, }) => { const client = await SignalWire({ host: process.env.RELAY_HOST, @@ -136,8 +143,14 @@ test.describe('CallFabric Relay Application', () => { }, }) + const reference = `e2e-relay-app_${uuid()}` + await resource.createRelayAppResource({ + name: reference, + reference, + }) + await client.voice.listen({ - topics: ['cf-e2e-test-relay'], + topics: [reference], onCallReceived: async (call) => { try { console.log('Call received', call.id) @@ -160,8 +173,6 @@ test.describe('CallFabric Relay Application', () => { await createCFClient(page) - const resourceName = 'cf-e2e-test-relay' - await page.evaluate( async (options) => { return new Promise(async (resolve, _reject) => { @@ -169,7 +180,7 @@ test.describe('CallFabric Relay Application', () => { const client = window._client const call = await client.dial({ - to: `/public/${options.resourceName}`, + to: `/private/${options.reference}`, rootElement: document.getElementById('rootElement'), }) @@ -179,7 +190,7 @@ test.describe('CallFabric Relay Application', () => { resolve(call) }) }, - { resourceName } + { reference } ) const callPlayStarted = page.evaluate(async () => { @@ -249,6 +260,7 @@ test.describe('CallFabric Relay Application', () => { test('should connect to the relay app and expect a hangup', async ({ createCustomPage, + resource, }) => { const client = await SignalWire({ host: process.env.RELAY_HOST, @@ -259,8 +271,14 @@ test.describe('CallFabric Relay Application', () => { }, }) + const reference = `e2e-relay-app_${uuid()}` + await resource.createRelayAppResource({ + name: reference, + reference, + }) + await client.voice.listen({ - topics: ['cf-e2e-test-relay'], + topics: [reference], onCallReceived: async (call) => { try { console.log('Call received', call.id) @@ -279,8 +297,6 @@ test.describe('CallFabric Relay Application', () => { const page = await createCustomPage({ name: '[page]' }) await page.goto(SERVER_URL) - const resourceName = 'cf-e2e-test-relay' - await createCFClient(page) await page.evaluate( @@ -290,7 +306,7 @@ test.describe('CallFabric Relay Application', () => { const client = window._client const call = await client.dial({ - to: `/public/${options.resourceName}`, + to: `/private/${options.reference}`, rootElement: document.getElementById('rootElement'), }) @@ -300,7 +316,7 @@ test.describe('CallFabric Relay Application', () => { resolve(call) }) }, - { resourceName } + { reference } ) const expectInitialEvents = expectCFInitialEvents(page) diff --git a/internal/e2e-js/tests/callfabric/swml.spec.ts b/internal/e2e-js/tests/callfabric/swml.spec.ts index cc78c6b17..ebeae4adf 100644 --- a/internal/e2e-js/tests/callfabric/swml.spec.ts +++ b/internal/e2e-js/tests/callfabric/swml.spec.ts @@ -1,3 +1,4 @@ +import { uuid } from '@signalwire/core' import { test } from '../../fixtures' import { SERVER_URL, @@ -8,14 +9,49 @@ import { } from '../../utils' test.describe('CallFabric SWML', () => { + const swmlTTS = { + sections: { + main: [ + 'answer', + { + play: { + volume: 10, + urls: [ + 'say:Hi', + 'say:Welcome to SignalWire', + "say:Thank you for calling us. All our lines are currently busy, but your call is important to us. Please hang up, and we'll return your call as soon as our representative is available.", + ], + }, + }, + ], + }, + } + const swmlHangup = { + version: '1.0.0', + sections: { + main: [ + 'answer', + { + hangup: { + reason: 'busy', + }, + }, + ], + }, + } + test('should dial an address and expect a TTS audio', async ({ createCustomPage, + resource, }) => { const page = await createCustomPage({ name: '[page]' }) await page.goto(SERVER_URL) - const resourceName = process.env.RESOURCE_NAME ?? '/public/cf-e2e-test-tts' - console.log(`#### Dialing ${resourceName}`) + const resourceName = `e2e-swml-app_${uuid()}` + await resource.createSWMLAppResource({ + name: resourceName, + contents: swmlTTS, + }) await createCFClient(page) @@ -28,7 +64,7 @@ test.describe('CallFabric SWML', () => { const client = window._client const call = await client.dial({ - to: resourceName, + to: `/private/${resourceName}`, rootElement: document.getElementById('rootElement'), }) @@ -40,18 +76,6 @@ test.describe('CallFabric SWML', () => { }, { resourceName } ) - page.expectWsTraffic({ - assertations: [ - { - type: "send", - name: "connect", - expect: { - method: "signalwire.connect", - "params.version.major": 4, - }, - } - ] - }) const callPlayStarted = page.evaluate(async () => { // @ts-expect-error @@ -101,12 +125,16 @@ test.describe('CallFabric SWML', () => { test('should dial an address and expect a hangup', async ({ createCustomPage, + resource, }) => { const page = await createCustomPage({ name: '[page]' }) await page.goto(SERVER_URL) - const resourceName = - process.env.RESOURCE_NAME ?? '/public/cf-e2e-test-hangup' + const resourceName = `e2e-swml-app_${uuid()}` + await resource.createSWMLAppResource({ + name: resourceName, + contents: swmlHangup, + }) await createCFClient(page) @@ -118,7 +146,7 @@ test.describe('CallFabric SWML', () => { const client = window._client const call = await client.dial({ - to: resourceName, + to: `/private/${resourceName}`, rootElement: document.getElementById('rootElement'), }) diff --git a/internal/e2e-js/tests/callfabric/videoRoom.spec.ts b/internal/e2e-js/tests/callfabric/videoRoom.spec.ts index 11d81b275..6d32921b4 100644 --- a/internal/e2e-js/tests/callfabric/videoRoom.spec.ts +++ b/internal/e2e-js/tests/callfabric/videoRoom.spec.ts @@ -1,3 +1,4 @@ +import { uuid } from '@signalwire/core' import { Video } from '@signalwire/js' import { test, expect } from '../../fixtures' import { @@ -12,11 +13,13 @@ import { test.describe('CallFabric VideoRoom', () => { test('should handle joining a room, perform actions and then leave the room', async ({ createCustomPage, + resource, }) => { const page = await createCustomPage({ name: '[page]' }) await page.goto(SERVER_URL) - const roomName = 'cf-e2e-test-room' + const roomName = `e2e-video-room_${uuid()}` + await resource.createVideoRoomResource(roomName) await createCFClient(page) @@ -269,11 +272,13 @@ test.describe('CallFabric VideoRoom', () => { test('should handle joining a room with audio channel only', async ({ createCustomPage, + resource, }) => { const page = await createCustomPage({ name: '[page]' }) await page.goto(SERVER_URL) - const roomName = 'cf-e2e-test-room' + const roomName = `e2e-video-room_${uuid()}` + await resource.createVideoRoomResource(roomName) await createCFClient(page) @@ -308,11 +313,10 @@ test.describe('CallFabric VideoRoom', () => { ) ).toBeTruthy() - const stats = await getStats(page) expect(stats.outboundRTP).not.toHaveProperty('video') - + expect(stats.inboundRTP.audio.packetsReceived).toBeGreaterThan(0) }) }) diff --git a/internal/e2e-js/utils.ts b/internal/e2e-js/utils.ts index 0b6bbfd8c..79e6dfb51 100644 --- a/internal/e2e-js/utils.ts +++ b/internal/e2e-js/utils.ts @@ -282,24 +282,6 @@ export const createTestSATToken = async () => { return data.token } -export const createVideoRoom = async (name?: string) => { - const response = await fetch( - `https://${process.env.API_HOST}/api/fabric/resources/video_rooms`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Basic ${BASIC_TOKEN}`, - }, - body: JSON.stringify({ - name: name ? name : `e2e-js-test-room_${uuid()}`, - }), - } - ) - const data = await response.json() - return data -} - interface CreateTestCRTOptions { ttl: number member_id: string @@ -735,59 +717,6 @@ export const expectPageReceiveMedia = async (page: Page, delay = 5_000) => { ) } -export const pageEmittedEvents = async ( - page: Page, - events: Record>[] -) => { - return page.evaluate( - async ({ events }) => { - // @ts-expect-error - const call = window._roomObj - - const expectationEvent = ( - expectation: Record> - ) => Object.keys(expectation)[0] - - const verifyExpectation = ( - rules: Record, - payload: any - ) => { - for (const [key, value] of Object.entries(rules)) { - if (payload[key] !== value) return false - } - - return true - } - - const eventsPromises = events.map((exp) => { - const event = expectationEvent(exp) - - console.log(`#### here is a promise for ${event}`) - return new Promise((res) => { - const callback = (payload: any) => { - console.log(`#### Event ${event} received`) - if (verifyExpectation(exp[event], payload)) { - call.off(event, callback) - console.log(`#### resolving ${event}`) - return res(payload) - } - } - console.log(`#### setting call.on ${event}`) - call.on(event, callback) - }) - }) - - try { - await Promise.all(eventsPromises) - return true - } catch {} - - return false - }, - { events } - ) -} - export const createCallWithCompatibilityApi = async ( resource: string, inlineLaml: string, @@ -828,7 +757,7 @@ export const createCallWithCompatibilityApi = async ( ) if (Number.isInteger(Number(response.status)) && response.status !== null) { - if (response.status !== 201) { + if (response.status !== 201) { console.log( 'Unexpected response from REST API: ', response.status, @@ -841,67 +770,6 @@ export const createCallWithCompatibilityApi = async ( return undefined } -export const expectv2TotalAudioEnergyToBeGreaterThan = async ( - page: Page, - value: number -) => { - const audioStats = await page.evaluate(async () => { - // @ts-expect-error - const currentCall = window.__currentCall - const audioReceiver = currentCall.peer.instance - .getReceivers() - .find((r: any) => r.track.kind === 'audio') - - const audioTrackId = audioReceiver.track.id - - const stats = await currentCall.peer.instance.getStats(null) - const filter = { - 'inbound-rtp': [ - 'audioLevel', - 'totalAudioEnergy', - 'totalSamplesDuration', - 'totalSamplesReceived', - 'packetsDiscarded', - 'lastPacketReceivedTimestamp', - 'bytesReceived', - 'packetsReceived', - 'packetsLost', - 'packetsRetransmitted', - ], - } - const result: any = {} - Object.keys(filter).forEach((entry) => { - result[entry] = {} - }) - - stats.forEach((report: any) => { - for (const [key, value] of Object.entries(filter)) { - if ( - report.type == key && - report['mediaType'] === 'audio' && - report['trackIdentifier'] === audioTrackId - ) { - value.forEach((entry) => { - if (report[entry]) { - result[key][entry] = report[entry] - } - }) - } - } - }, {}) - - return result - }) - console.log('audioStats: ', audioStats) - - const totalAudioEnergy = audioStats['inbound-rtp']['totalAudioEnergy'] - if (totalAudioEnergy) { - expect(totalAudioEnergy).toBeGreaterThan(value) - } else { - console.log('Warning - totalAudioEnergy was not present in the audioStats.') - } -} - export const getDialConferenceLaml = (conferenceNameBase: string) => { const conferenceName = randomizeRoomName(conferenceNameBase) const conferenceRegion = process.env.LAML_CONFERENCE_REGION ?? '' @@ -1100,7 +968,8 @@ export const expectedMinPackets = ( maxMissingPacketsTolerance = 1 } - const minPackets = (callDurationMs * (1 - maxMissingPacketsTolerance)) * packetRate / 1000 + const minPackets = + (callDurationMs * (1 - maxMissingPacketsTolerance) * packetRate) / 1000 return minPackets } @@ -1217,3 +1086,99 @@ export const expectCFFinalEvents = ( return Promise.all([finalEvents, ...extraEvents]) } + +export interface Resource { + id: string + project_id: string + type: string + display_name: string + created_at: string +} + +export const createVideoRoomResource = async (name?: string) => { + const response = await fetch( + `https://${process.env.API_HOST}/api/fabric/resources/video_rooms`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${BASIC_TOKEN}`, + }, + body: JSON.stringify({ + name: name ?? `e2e-test-room_${uuid()}`, + }), + } + ) + const data = (await response.json()) as Resource + console.log('>> Resource VideoRoom created:', data.id, name) + return data +} + +export interface CreateSWMLAppResourceParams { + name?: string + contents: Record +} +export const createSWMLAppResource = async ({ + name, + contents, +}: CreateSWMLAppResourceParams) => { + const response = await fetch( + `https://${process.env.API_HOST}/api/fabric/resources/swml_applications`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${BASIC_TOKEN}`, + }, + body: JSON.stringify({ + name: name ?? `e2e-swml-app_${uuid()}`, + handle_calls_using: 'script', + call_handler_script: JSON.stringify(contents), + }), + } + ) + const data = (await response.json()) as Resource + console.log('>> Resource SWML App created:', data.id) + return data +} + +export interface CreateRelayAppResourceParams { + name?: string + reference: string +} +export const createRelayAppResource = async ({ + name, + reference, +}: CreateRelayAppResourceParams) => { + const response = await fetch( + `https://${process.env.API_HOST}/api/fabric/resources/relay_applications`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${BASIC_TOKEN}`, + }, + body: JSON.stringify({ + name: name ?? `e2e-relay-app_${uuid()}`, + reference, + }), + } + ) + const data = (await response.json()) as Resource + console.log('>> Resource Relay App created:', data.id) + return data +} + +export const deleteResource = async (id: string) => { + const response = await fetch( + `https://${process.env.API_HOST}/api/fabric/resources/${id}`, + { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${BASIC_TOKEN}`, + }, + } + ) + return response +} diff --git a/internal/e2e-realtime-api/src/utils.ts b/internal/e2e-realtime-api/src/utils.ts index 1fddd852a..6751c3331 100644 --- a/internal/e2e-realtime-api/src/utils.ts +++ b/internal/e2e-realtime-api/src/utils.ts @@ -328,12 +328,6 @@ export const sessionStorageMock = () => { } } -export const sleep = (ms = 3000) => { - return new Promise((r) => { - setTimeout(r, ms) - }) -} - export const makeSipDomainAppAddress = ({ name, domain }) => { return `sip:${name}-${randomBytes(16).toString('hex')}@${domain}.${ process.env.DAPP_DOMAIN diff --git a/internal/e2e-realtime-api/src/voiceRecord/withDialListeners.test.ts b/internal/e2e-realtime-api/src/voiceRecord/withDialListeners.test.ts index 5250b8337..7db04a4c6 100644 --- a/internal/e2e-realtime-api/src/voiceRecord/withDialListeners.test.ts +++ b/internal/e2e-realtime-api/src/voiceRecord/withDialListeners.test.ts @@ -4,7 +4,6 @@ import { createTestRunner, CALL_RECORD_PROPS, CALL_PROPS, - sleep, TestHandler, makeSipDomainAppAddress, } from '../utils'