Storage service tests and benches in ts/test-mock
This commit is contained in:
parent
48137a498c
commit
6281d52ec6
20 changed files with 1866 additions and 100 deletions
286
ts/test-mock/bootstrap.ts
Normal file
286
ts/test-mock/bootstrap.ts
Normal file
|
@ -0,0 +1,286 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import assert from 'assert';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import createDebug from 'debug';
|
||||
|
||||
import type { Device, PrimaryDevice } from '@signalapp/mock-server';
|
||||
import { Server, loadCertificates } from '@signalapp/mock-server';
|
||||
import { App } from './playwright';
|
||||
import * as durations from '../util/durations';
|
||||
|
||||
const debug = createDebug('mock:bootstrap');
|
||||
|
||||
const ELECTRON = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'node_modules',
|
||||
'.bin',
|
||||
'electron'
|
||||
);
|
||||
const CI_SCRIPT = path.join(__dirname, '..', '..', 'ci.js');
|
||||
|
||||
const CLOSE_TIMEOUT = 10 * 1000;
|
||||
|
||||
const CONTACT_FIRST_NAMES = [
|
||||
'Alice',
|
||||
'Bob',
|
||||
'Charlie',
|
||||
'Paul',
|
||||
'Steve',
|
||||
'William',
|
||||
];
|
||||
const CONTACT_LAST_NAMES = [
|
||||
'Smith',
|
||||
'Brown',
|
||||
'Jones',
|
||||
'Miller',
|
||||
'Davis',
|
||||
'Lopez',
|
||||
'Gonazales',
|
||||
];
|
||||
|
||||
const CONTACT_NAMES = new Array<string>();
|
||||
for (const firstName of CONTACT_FIRST_NAMES) {
|
||||
for (const lastName of CONTACT_LAST_NAMES) {
|
||||
CONTACT_NAMES.push(`${firstName} ${lastName}`);
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_CONTACTS = CONTACT_NAMES.length;
|
||||
|
||||
export type BootstrapOptions = Readonly<{
|
||||
extraConfig?: Record<string, unknown>;
|
||||
benchmark?: boolean;
|
||||
|
||||
linkedDevices?: number;
|
||||
contactCount?: number;
|
||||
}>;
|
||||
|
||||
type BootstrapInternalOptions = Pick<BootstrapOptions, 'extraConfig'> &
|
||||
Readonly<{
|
||||
benchmark: boolean;
|
||||
linkedDevices: number;
|
||||
contactCount: number;
|
||||
}>;
|
||||
|
||||
//
|
||||
// Bootstrap is a class that prepares mock server and desktop for running
|
||||
// tests/benchmarks.
|
||||
//
|
||||
// In general, the usage pattern is:
|
||||
//
|
||||
// const bootstrap = new Bootstrap();
|
||||
// await bootstrap.init();
|
||||
// const app = await bootstrap.link();
|
||||
// await bootstrap.teardown();
|
||||
//
|
||||
// Once initialized `bootstrap` variable will have following useful properties:
|
||||
//
|
||||
// - `server` - a mock server instance
|
||||
// - `desktop` - a linked device representing currently running desktop instance
|
||||
// - `phone` - a primary device representing desktop's primary
|
||||
// - `contacts` - a list of primary devices for contacts that are synced over
|
||||
// through contact sync
|
||||
//
|
||||
// `bootstrap.getTimestamp()` could be used to generate consecutive timestamp
|
||||
// for sending messages.
|
||||
//
|
||||
// All phone numbers and uuids for all contacts and ourselves are random and not
|
||||
// the same between different test runs.
|
||||
//
|
||||
export class Bootstrap {
|
||||
public readonly server = new Server();
|
||||
|
||||
private readonly options: BootstrapInternalOptions;
|
||||
private privContacts?: ReadonlyArray<PrimaryDevice>;
|
||||
private privPhone?: PrimaryDevice;
|
||||
private privDesktop?: Device;
|
||||
private storagePath?: string;
|
||||
private timestamp: number = Date.now() - durations.MONTH;
|
||||
|
||||
constructor(options: BootstrapOptions = {}) {
|
||||
this.options = {
|
||||
linkedDevices: 5,
|
||||
contactCount: MAX_CONTACTS,
|
||||
benchmark: false,
|
||||
|
||||
...options,
|
||||
};
|
||||
|
||||
assert(this.options.contactCount <= MAX_CONTACTS);
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
debug('initializing');
|
||||
|
||||
await this.server.listen(0);
|
||||
|
||||
const { port } = this.server.address();
|
||||
debug('started server on port=%d', port);
|
||||
|
||||
const contactNames = CONTACT_NAMES.slice(0, this.options.contactCount);
|
||||
|
||||
this.privContacts = await Promise.all(
|
||||
contactNames.map(async profileName => {
|
||||
const primary = await this.server.createPrimaryDevice({ profileName });
|
||||
|
||||
for (let i = 0; i < this.options.linkedDevices; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.server.createSecondaryDevice(primary);
|
||||
}
|
||||
|
||||
return primary;
|
||||
})
|
||||
);
|
||||
|
||||
this.privPhone = await this.server.createPrimaryDevice({
|
||||
profileName: 'Mock',
|
||||
contacts: this.contacts,
|
||||
});
|
||||
|
||||
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
|
||||
|
||||
debug('setting storage path=%j', this.storagePath);
|
||||
}
|
||||
|
||||
public async teardown(): Promise<void> {
|
||||
debug('tearing down');
|
||||
|
||||
await Promise.race([
|
||||
this.storagePath
|
||||
? fs.rm(this.storagePath, { recursive: true })
|
||||
: Promise.resolve(),
|
||||
this.server.close(),
|
||||
new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT).unref()),
|
||||
]);
|
||||
}
|
||||
|
||||
public async link(): Promise<App> {
|
||||
debug('linking');
|
||||
|
||||
const app = await this.startApp();
|
||||
|
||||
const provision = await this.server.waitForProvision();
|
||||
|
||||
const provisionURL = await app.waitForProvisionURL();
|
||||
|
||||
this.privDesktop = await provision.complete({
|
||||
provisionURL,
|
||||
primaryDevice: this.phone,
|
||||
});
|
||||
|
||||
debug('new desktop device %j', this.desktop.debugId);
|
||||
|
||||
const desktopKey = await this.desktop.popSingleUseKey();
|
||||
await this.phone.addSingleUseKey(this.desktop, desktopKey);
|
||||
|
||||
for (const contact of this.contacts) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const contactKey = await this.desktop.popSingleUseKey();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await contact.addSingleUseKey(this.desktop, contactKey);
|
||||
}
|
||||
|
||||
await this.phone.waitForSync(this.desktop);
|
||||
this.phone.resetSyncState(this.desktop);
|
||||
|
||||
debug('synced with %j', this.desktop.debugId);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public async linkAndClose(): Promise<void> {
|
||||
const app = await this.link();
|
||||
|
||||
debug('closing the app after link');
|
||||
await app.close();
|
||||
}
|
||||
|
||||
public async startApp(): Promise<App> {
|
||||
assert(
|
||||
this.storagePath !== undefined,
|
||||
'Bootstrap has to be initialized first, see: bootstrap.init()'
|
||||
);
|
||||
|
||||
debug('starting the app');
|
||||
|
||||
const { port } = this.server.address();
|
||||
|
||||
const app = new App({
|
||||
main: ELECTRON,
|
||||
args: [CI_SCRIPT],
|
||||
config: await this.generateConfig(port),
|
||||
});
|
||||
|
||||
await app.start();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
const result = this.timestamp;
|
||||
this.timestamp += 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// Getters
|
||||
//
|
||||
|
||||
public get phone(): PrimaryDevice {
|
||||
assert(
|
||||
this.privPhone,
|
||||
'Bootstrap has to be initialized first, see: bootstrap.init()'
|
||||
);
|
||||
return this.privPhone;
|
||||
}
|
||||
|
||||
public get desktop(): Device {
|
||||
assert(
|
||||
this.privDesktop,
|
||||
'Bootstrap has to be linked first, see: bootstrap.link()'
|
||||
);
|
||||
return this.privDesktop;
|
||||
}
|
||||
|
||||
public get contacts(): ReadonlyArray<PrimaryDevice> {
|
||||
assert(
|
||||
this.privContacts,
|
||||
'Bootstrap has to be initialized first, see: bootstrap.init()'
|
||||
);
|
||||
return this.privContacts;
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
||||
private async generateConfig(port: number): Promise<string> {
|
||||
const url = `https://mock.signal.org:${port}`;
|
||||
return JSON.stringify({
|
||||
...(await loadCertificates()),
|
||||
|
||||
forcePreloadBundle: this.options.benchmark,
|
||||
enableCI: true,
|
||||
|
||||
buildExpiration: Date.now() + durations.MONTH,
|
||||
storagePath: this.storagePath,
|
||||
storageProfile: 'mock',
|
||||
serverUrl: url,
|
||||
storageUrl: url,
|
||||
directoryUrl: url,
|
||||
cdn: {
|
||||
'0': url,
|
||||
'2': url,
|
||||
},
|
||||
updatesEnabled: false,
|
||||
|
||||
...this.options.extraConfig,
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue