From b95161859e211abb9732c133e239a15df438a959 Mon Sep 17 00:00:00 2001 From: Miriam Zimmerman Date: Fri, 11 Jul 2025 11:48:31 -0400 Subject: [PATCH] Add integration test for call decline --- .github/workflows/ci.yml | 3 +- package.json | 3 +- pnpm-lock.yaml | 10 +- .../calling/callMessages_test.docker.ts | 110 ++++++++++++++++++ ts/test-mock/calling/helpers.ts | 32 +++++ .../twoClients_test.ts} | 6 +- ts/test-mock/playwright.ts | 4 + 7 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 ts/test-mock/calling/callMessages_test.docker.ts create mode 100644 ts/test-mock/calling/helpers.ts rename ts/test-mock/{calling/callMessages_test.ts => messaging/twoClients_test.ts} (95%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a66b4686c0..8e5783be2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -429,7 +429,8 @@ jobs: run: | set -o pipefail xvfb-run --auto-servernum pnpm run test-mock - timeout-minutes: 10 + xvfb-run --auto-servernum pnpm run test-mock-docker + timeout-minutes: 15 env: NODE_ENV: production DEBUG: mock:test:* diff --git a/package.json b/package.json index dcb9d2f01d..5718a7fbd9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test-release": "node ts/scripts/test-release.js", "test-node": "cross-env LANG=en-us electron-mocha --timeout 10000 --main test/fix-linux-gtk.js --file test/setup-test-node.js --recursive ts/test-node", "test-mock": "mocha --require ts/test-mock/setup-ci.js ts/test-mock/**/*_test.js", + "test-mock-docker": "mocha --require ts/test-mock/setup-ci.js ts/test-mock/**/*_test.docker.js", "test-eslint": "mocha .eslint/rules/**/*.test.js --ignore-leaks", "test-lint-intl": "ts-node ./build/intl-linter/linter.ts --test", "eslint": "eslint --cache . --cache-strategy content --max-warnings 0", @@ -222,7 +223,7 @@ "@indutny/parallel-prettier": "3.0.0", "@indutny/rezip-electron": "2.0.1", "@napi-rs/canvas": "0.1.61", - "@signalapp/mock-server": "13.1.0", + "@signalapp/mock-server": "13.2.0", "@storybook/addon-a11y": "8.4.4", "@storybook/addon-actions": "8.4.4", "@storybook/addon-controls": "8.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36b80304c2..d15d2b8833 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -430,8 +430,8 @@ importers: specifier: 0.1.61 version: 0.1.61 '@signalapp/mock-server': - specifier: 13.1.0 - version: 13.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: 13.2.0 + version: 13.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@storybook/addon-a11y': specifier: 8.4.4 version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10)) @@ -2770,8 +2770,8 @@ packages: '@signalapp/libsignal-client@0.76.3': resolution: {integrity: sha512-Ht8XtdsSvgiCb8ftUYE9DaLcWy0vltrj9cQ2sfy+DGUayE1k2njicNhB2RKOfQV2Wb/1Cl0WxVZP/NlXRo2+jA==} - '@signalapp/mock-server@13.1.0': - resolution: {integrity: sha512-CuDNLNEBMzwIs5jr7Lx9F4YFoRD62s7WgPGtm3qpaggixSQtabjMC7AKSR0xvaHcZpYZtBU5jcGK8Roguo9nuw==} + '@signalapp/mock-server@13.2.0': + resolution: {integrity: sha512-f5uxzsIwPmkevX5ycCRWgqy/VCbvj/dUA8yGWlfO2hFc62UZ4FaOY0n5YFMOpQgp5eMO6TMgr1KqIR/uAxfkIg==} '@signalapp/parchment-cjs@3.0.1': resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==} @@ -12466,7 +12466,7 @@ snapshots: type-fest: 4.26.1 uuid: 11.0.2 - '@signalapp/mock-server@13.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@signalapp/mock-server@13.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@indutny/parallel-prettier': 3.0.0(prettier@3.3.3) '@signalapp/libsignal-client': 0.60.2 diff --git a/ts/test-mock/calling/callMessages_test.docker.ts b/ts/test-mock/calling/callMessages_test.docker.ts new file mode 100644 index 0000000000..b81132f351 --- /dev/null +++ b/ts/test-mock/calling/callMessages_test.docker.ts @@ -0,0 +1,110 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { StorageState } from '@signalapp/mock-server'; +import { expect } from 'playwright/test'; +import * as durations from '../../util/durations'; +import type { App } from '../playwright'; +import { Bootstrap } from '../bootstrap'; +import { runTurnInContainer, tearDownTurnContainer } from './helpers'; + +describe('callMessages', function callMessages(this: Mocha.Suite) { + this.timeout(durations.MINUTE); + + let bootstrap1: Bootstrap; + let bootstrap2: Bootstrap; + let app1: App; + let app2: App; + + beforeEach(async () => { + runTurnInContainer(); + + bootstrap1 = new Bootstrap(); + await bootstrap1.init(); + + bootstrap2 = new Bootstrap({ server: bootstrap1.server }); + await bootstrap2.init(); + + let state1 = StorageState.getEmpty(); + state1 = state1.updateAccount({ + profileKey: bootstrap1.phone.profileKey.serialize(), + }); + + state1 = state1.addContact(bootstrap2.phone, { + whitelisted: true, + profileKey: bootstrap2.phone.profileKey.serialize(), + givenName: 'Contact2', + }); + + state1 = state1.pin(bootstrap2.phone); + + await bootstrap1.phone.setStorageState(state1); + + app1 = await bootstrap1.link(); + + let state2 = StorageState.getEmpty(); + state2 = state2.updateAccount({ + profileKey: bootstrap2.phone.profileKey.serialize(), + }); + + state2 = state2.addContact(bootstrap1.phone, { + whitelisted: true, + profileKey: bootstrap1.phone.profileKey.serialize(), + givenName: 'Contact1', + }); + + state2 = state2.pin(bootstrap1.phone); + await bootstrap2.phone.setStorageState(state2); + + app2 = await bootstrap2.link(); + }); + + afterEach(async function after(this: Mocha.Context) { + tearDownTurnContainer(); + + if (!bootstrap1) { + return; + } + await bootstrap1.maybeSaveLogs(this.currentTest, app1); + await bootstrap2.maybeSaveLogs(this.currentTest, app2); + + await app2.close(); + await app1.close(); + + await bootstrap2.teardown(); + await bootstrap1.teardown(); + }); + + it('can call and decline a call', async () => { + const window1 = await app1.getWindow(); + const leftPane1 = window1.locator('#LeftPane'); + + await leftPane1 + .locator(`[data-testid="${bootstrap2.phone.device.aci}"]`) + .click(); + // Try to start a call + await window1.locator('.module-ConversationHeader__button--audio').click(); + const window1Permissions = await app1.waitForWindow(); + await window1Permissions.getByText('Allow Access').click(); + await window1 + .locator('.CallingLobbyJoinButton') + .and(window1.locator('button:visible')) + .click(); + + const window2 = await app2.getWindow(); + + // Only wait for 3 seconds to make sure that this succeeded properly rather + // than timing out after ~10 seconds and using a direct connection + await window2 + .locator('.IncomingCallBar__button--decline') + .click({ timeout: 3000 }); + + await expect( + window1.locator('.module-calling__modal-container') + ).toBeEmpty(); + + await expect( + window2.locator('.module-calling__modal-container') + ).toBeEmpty(); + }); +}); diff --git a/ts/test-mock/calling/helpers.ts b/ts/test-mock/calling/helpers.ts new file mode 100644 index 0000000000..9672b0c74f --- /dev/null +++ b/ts/test-mock/calling/helpers.ts @@ -0,0 +1,32 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as child from 'child_process'; +import createDebug from 'debug'; + +const debug = createDebug('mock:test:calling:helpers'); + +export function runTurnInContainer(): void { + tearDownTurnContainer(); + const result = child.spawnSync('docker', [ + 'run', + '--name', + 'coturn', + '-d', + '--network=host', + 'coturn/coturn', + ]); + debug( + 'create coturn: signal: ', + result.signal, + ' status: ', + result.status, + 'stderr: ', + result.stderr?.toString() + ); +} + +export function tearDownTurnContainer(): void { + debug('tearDownTurnContainer'); + child.spawnSync('docker', ['rm', '--force', '--volumes', 'coturn']); +} diff --git a/ts/test-mock/calling/callMessages_test.ts b/ts/test-mock/messaging/twoClients_test.ts similarity index 95% rename from ts/test-mock/calling/callMessages_test.ts rename to ts/test-mock/messaging/twoClients_test.ts index b25feed9cb..1bdc4f4bc9 100644 --- a/ts/test-mock/calling/callMessages_test.ts +++ b/ts/test-mock/messaging/twoClients_test.ts @@ -7,7 +7,7 @@ import type { App } from '../playwright'; import { Bootstrap } from '../bootstrap'; import { typeIntoInput, waitForEnabledComposer } from '../helpers'; -describe('callMessages', function (this: Mocha.Suite) { +describe('twoClients', function twoClients(this: Mocha.Suite) { this.timeout(durations.MINUTE); let bootstrap1: Bootstrap; @@ -56,12 +56,12 @@ describe('callMessages', function (this: Mocha.Suite) { app2 = await bootstrap2.link(); }); - afterEach(async function (this: Mocha.Context) { + afterEach(async function after(this: Mocha.Context) { if (!bootstrap1) { return; } - await bootstrap2.maybeSaveLogs(this.currentTest, app2); await bootstrap1.maybeSaveLogs(this.currentTest, app1); + await bootstrap2.maybeSaveLogs(this.currentTest, app2); await app2.close(); await app1.close(); diff --git a/ts/test-mock/playwright.ts b/ts/test-mock/playwright.ts index aec7be460e..4b4cacdd52 100644 --- a/ts/test-mock/playwright.ts +++ b/ts/test-mock/playwright.ts @@ -145,6 +145,10 @@ export class App extends EventEmitter { return this.#waitForEvent('storageServiceComplete'); } + public async waitForWindow(): Promise { + return this.#app.waitForEvent('window'); + } + public async waitForManifestVersion(version: number): Promise { // eslint-disable-next-line no-constant-condition while (true) {