Adapt bootstrap to support two clients

This commit is contained in:
Miriam Zimmerman 2025-07-02 19:11:41 -04:00 committed by GitHub
parent 5044b3ca3b
commit 9963daf3bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 128 additions and 21 deletions

View file

@ -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",

10
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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<PrimaryDevice>;
@ -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<void> {
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()),

View file

@ -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();
});
});