diff --git a/package.json b/package.json index 56614a9132..d3bfaff84d 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,7 @@ "@indutny/parallel-prettier": "3.0.0", "@indutny/rezip-electron": "2.0.1", "@napi-rs/canvas": "0.1.61", - "@signalapp/mock-server": "13.0.1", + "@signalapp/mock-server": "13.1.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 0963bb4e34..4a09d13a65 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.0.1 - version: 13.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: 13.1.0 + version: 13.1.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.0': resolution: {integrity: sha512-wQZFC79GAUeee8pf+aDK5Gii0HbQoCAv/oTn1Ht7d5mFq2pw/L0jRcv3j9DgVYodzCOlnanfto3apfA6eN/Whw==} - '@signalapp/mock-server@13.0.1': - resolution: {integrity: sha512-1rT0fYyqEad64GnZRrFVhNsgKpPS+pvyyk8iOGUHqnqnf818yLIYHblS/5m/cNcvHyC/BBqdtgRHAsfGNqkuZw==} + '@signalapp/mock-server@13.1.0': + resolution: {integrity: sha512-CuDNLNEBMzwIs5jr7Lx9F4YFoRD62s7WgPGtm3qpaggixSQtabjMC7AKSR0xvaHcZpYZtBU5jcGK8Roguo9nuw==} '@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.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@signalapp/mock-server@13.1.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/bootstrap.ts b/ts/test-mock/bootstrap.ts index 13c57c42ca..41361b14cd 100644 --- a/ts/test-mock/bootstrap.ts +++ b/ts/test-mock/bootstrap.ts @@ -117,6 +117,9 @@ export type BootstrapOptions = Readonly<{ contactPreKeyCount?: number; useLegacyStorageEncryption?: boolean; + + // Optional. specify a server to use instead of creating and initializing one. + server?: Server; }>; export type EphemeralBackupType = Readonly< @@ -207,7 +210,7 @@ const DEFAULT_REMOTE_CONFIG = [ // export class Bootstrap { public readonly server: Server; - public readonly cdn3Path: string; + public readonly cdn3Path?: string; readonly #options: BootstrapInternalOptions; #privContacts?: ReadonlyArray; @@ -221,16 +224,18 @@ export class Bootstrap { readonly #randomId = crypto.randomBytes(8).toString('hex'); constructor(options: BootstrapOptions = {}) { - this.cdn3Path = path.join( - os.tmpdir(), - `mock-signal-cdn3-${this.#randomId}` - ); - this.server = new Server({ - // Limit number of storage read keys for easier testing - maxStorageReadKeys: MAX_STORAGE_READ_KEYS, - cdn3Path: this.cdn3Path, - updates2Path: path.join(__dirname, 'updates-data'), - }); + this.cdn3Path = + options.server === undefined + ? path.join(os.tmpdir(), `mock-signal-cdn3-${this.#randomId}`) + : undefined; + this.server = + options.server ?? + new Server({ + // Limit number of storage read keys for easier testing + maxStorageReadKeys: MAX_STORAGE_READ_KEYS, + cdn3Path: this.cdn3Path, + updates2Path: path.join(__dirname, 'updates-data'), + }); this.#options = { linkedDevices: 5, @@ -254,10 +259,14 @@ export class Bootstrap { public async init(): Promise { debug('initializing'); - await this.server.listen(0); + if (this.#options.server === undefined) { + await this.server.listen(0); - const { port } = this.server.address(); - debug('started server on port=%d', port); + const { port } = this.server.address(); + debug('started server on port=%d', port); + } else { + debug('existing server listening on port = ', this.server.address().port); + } const totalContactCount = this.#options.contactCount + @@ -367,7 +376,9 @@ export class Bootstrap { ...[this.#storagePath, this.cdn3Path].map(tmpPath => tmpPath ? fs.rm(tmpPath, { recursive: true }) : Promise.resolve() ), - this.server.close(), + this.#options.server === undefined + ? this.server.close() + : Promise.resolve(), this.#lastApp?.close(), ]), new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT).unref()), diff --git a/ts/test-mock/calling/callMessages_test.ts b/ts/test-mock/calling/callMessages_test.ts new file mode 100644 index 0000000000..b25feed9cb --- /dev/null +++ b/ts/test-mock/calling/callMessages_test.ts @@ -0,0 +1,96 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { StorageState } from '@signalapp/mock-server'; +import * as durations from '../../util/durations'; +import type { App } from '../playwright'; +import { Bootstrap } from '../bootstrap'; +import { typeIntoInput, waitForEnabledComposer } from '../helpers'; + +describe('callMessages', function (this: Mocha.Suite) { + this.timeout(durations.MINUTE); + + let bootstrap1: Bootstrap; + let bootstrap2: Bootstrap; + let app1: App; + let app2: App; + + beforeEach(async () => { + 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 (this: Mocha.Context) { + if (!bootstrap1) { + return; + } + await bootstrap2.maybeSaveLogs(this.currentTest, app2); + await bootstrap1.maybeSaveLogs(this.currentTest, app1); + + await app2.close(); + await app1.close(); + + await bootstrap2.teardown(); + await bootstrap1.teardown(); + }); + + it('can send a message from one client to another', async () => { + const window1 = await app1.getWindow(); + const leftPane1 = window1.locator('#LeftPane'); + + await leftPane1 + .locator(`[data-testid="${bootstrap2.phone.device.aci}"]`) + .click(); + const window2 = await app2.getWindow(); + + const messageBody = 'Hello world'; + const compositionInput = await waitForEnabledComposer(window1); + await typeIntoInput(compositionInput, messageBody, ''); + await compositionInput.press('Enter'); + + const leftPane = window2.locator('#LeftPane'); + await leftPane + .locator(`[data-testid="${bootstrap1.phone.device.aci}"]`) + .click(); + + await window2 + .locator(`.module-message__text >> "${messageBody}"`) + .waitFor(); + }); +});