From 6281d52ec6fbe3fdb85d0581314892d2f7658042 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Fri, 11 Feb 2022 14:32:51 -0800 Subject: [PATCH] Storage service tests and benches in ts/test-mock --- .github/workflows/benchmark.yml | 67 +---- .github/workflows/ci.yml | 35 +-- app/main.ts | 8 +- ci.js | 8 + config/default.json | 1 + package.json | 4 + ts/test-mock/benchmarks/convo_open_bench.ts | 101 +++++++ ts/test-mock/benchmarks/fixtures.ts | 67 +++++ ts/test-mock/benchmarks/group_send_bench.ts | 184 ++++++++++++ ts/test-mock/benchmarks/send_bench.ts | 135 +++++++++ ts/test-mock/benchmarks/startup_bench.ts | 133 +++++++++ ts/test-mock/bootstrap.ts | 286 +++++++++++++++++++ ts/test-mock/playwright.ts | 85 ++++++ ts/test-mock/storage/archive_test.ts | 123 ++++++++ ts/test-mock/storage/fixtures.ts | 98 +++++++ ts/test-mock/storage/message_request_test.ts | 115 ++++++++ ts/test-mock/storage/pin_unpin_test.ts | 160 +++++++++++ ts/util/lint/exceptions.json | 218 ++++++++++++++ ts/util/lint/linter.ts | 1 + yarn.lock | 137 ++++++++- 20 files changed, 1866 insertions(+), 100 deletions(-) create mode 100644 ci.js create mode 100644 ts/test-mock/benchmarks/convo_open_bench.ts create mode 100644 ts/test-mock/benchmarks/fixtures.ts create mode 100644 ts/test-mock/benchmarks/group_send_bench.ts create mode 100644 ts/test-mock/benchmarks/send_bench.ts create mode 100644 ts/test-mock/benchmarks/startup_bench.ts create mode 100644 ts/test-mock/bootstrap.ts create mode 100644 ts/test-mock/playwright.ts create mode 100644 ts/test-mock/storage/archive_test.ts create mode 100644 ts/test-mock/storage/fixtures.ts create mode 100644 ts/test-mock/storage/message_request_test.ts create mode 100644 ts/test-mock/storage/pin_unpin_test.ts diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 780e44dee..19429bd9b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -20,18 +20,8 @@ jobs: - name: Get other system specs run: uname -a - - name: Configure git to use HTTPS - run: git config --global url."https://${{ secrets.AUTOMATED_GITHUB_PAT }}:x-oauth-basic@github.com".insteadOf ssh://git@github.com - - name: Clone Desktop repo uses: actions/checkout@v2 - - name: Clone Mock-Server repo - uses: actions/checkout@v2 - with: - repository: 'signalapp/Mock-Signal-Server-Private' - path: 'Mock-Server' - ref: 'gamma' - token: ${{ secrets.AUTOMATED_GITHUB_PAT }} - name: Setup node.js uses: actions/setup-node@v2 @@ -54,104 +44,63 @@ jobs: if: steps.cache-desktop-modules.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile - - name: Install Mock-Server node_modules - run: yarn install --frozen-lockfile - working-directory: Mock-Server - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - - name: Build typescript run: yarn generate - name: Bundle run: yarn build:webpack - - name: Copy CI configuration - run: | - cp -rf ./Mock-Server/config/local-development.json \ - ./config/local-development.json - cp -rf ./config/local-development.json ./config/local-production.json - - name: Setup hosts run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts - name: Run startup benchmarks run: | set -o pipefail - rm -rf /tmp/mock - xvfb-run --auto-servernum node Mock-Server/scripts/load-test.js \ - ./node_modules/.bin/electron . | tee benchmark-startup.log + xvfb-run --auto-servernum node ts/test-mock/benchmarks/startup_bench.js | + tee benchmark-startup.log timeout-minutes: 10 env: NODE_ENV: production RUN_COUNT: 10 ELECTRON_ENABLE_STACK_DUMPING: on - - name: Upload startup benchmark logs on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: startup-logs - path: /tmp/mock/logs - - name: Run send benchmarks run: | set -o pipefail rm -rf /tmp/mock - xvfb-run --auto-servernum node Mock-Server/scripts/send-test.js \ - ./node_modules/.bin/electron . | tee benchmark-send.log + xvfb-run --auto-servernum node ts/test-mock/benchmarks/send_bench.js | + tee benchmark-send.log timeout-minutes: 10 env: NODE_ENV: production RUN_COUNT: 100 ELECTRON_ENABLE_STACK_DUMPING: on - - name: Upload send benchmark logs on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: send-logs - path: /tmp/mock/logs - - name: Run group send benchmarks run: | set -o pipefail rm -rf /tmp/mock xvfb-run --auto-servernum node \ - Mock-Server/scripts/group-send-test.js \ - ./node_modules/.bin/electron . | tee benchmark-group-send.log + ts/test-mock/benchmarks/group_send_bench.js | \ + tee benchmark-group-send.log timeout-minutes: 10 env: NODE_ENV: production RUN_COUNT: 100 ELECTRON_ENABLE_STACK_DUMPING: on - - name: Upload group send benchmark logs on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: group-send-logs - path: /tmp/mock/logs - - name: Run conversation open benchmarks run: | set -o pipefail rm -rf /tmp/mock xvfb-run --auto-servernum node \ - Mock-Server/scripts/convo-open-test.js \ - ./node_modules/.bin/electron . | tee benchmark-convo-open.log + ts/test-mock/benchmarks/convo_open_bench.js | \ + tee benchmark-convo-open.log timeout-minutes: 10 env: NODE_ENV: production RUN_COUNT: 100 ELECTRON_ENABLE_STACK_DUMPING: on - - name: Upload conversation open benchmark logs on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: convo-open-logs - path: /tmp/mock/logs - - name: Clone benchmark repo uses: actions/checkout@v2 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f15ccff97..4e10de085 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,18 +161,8 @@ jobs: - name: Get other system specs run: uname -a - - name: Configure git to use HTTPS - run: git config --global url."https://${{ secrets.AUTOMATED_GITHUB_PAT }}:x-oauth-basic@github.com".insteadOf ssh://git@github.com - - name: Clone Desktop repo uses: actions/checkout@v2 - - name: Clone Mock-Server repo - uses: actions/checkout@v2 - with: - repository: 'signalapp/Mock-Signal-Server-Private' - path: 'Mock-Server' - ref: 'gamma' - token: ${{ secrets.AUTOMATED_GITHUB_PAT }} - name: Setup node.js uses: actions/setup-node@v2 @@ -195,40 +185,19 @@ jobs: if: steps.cache-desktop-modules.outputs.cache-hit != 'true' run: yarn install --frozen-lockfile - - name: Install Mock-Server node_modules - run: yarn install --frozen-lockfile - working-directory: Mock-Server - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - - name: Build typescript run: yarn generate - name: Bundle run: yarn build:webpack - - name: Copy CI configuration - run: | - cp -rf ./Mock-Server/config/local-development.json \ - ./config/local-development.json - cp -rf ./config/local-development.json ./config/local-production.json - - name: Setup hosts run: sudo echo "127.0.0.1 mock.signal.org" | sudo tee -a /etc/hosts - name: Run storage service tests run: | set -o pipefail - rm -rf /tmp/mock - xvfb-run --auto-servernum node Mock-Server/scripts/storage-service-test.js \ - ./node_modules/.bin/electron . + xvfb-run --auto-servernum yarn test-mock timeout-minutes: 10 env: NODE_ENV: production - DEBUG: mock:scripts:* - - - name: Upload logs on failure - if: failure() - uses: actions/upload-artifact@v2 - with: - name: logs - path: /tmp/mock/logs + DEBUG: mock:test-storage diff --git a/app/main.ts b/app/main.ts index 3d59176c7..282bf95bf 100644 --- a/app/main.ts +++ b/app/main.ts @@ -122,6 +122,7 @@ const development = const isThrottlingEnabled = development || isAlpha(app.getVersion()); const enableCI = config.get('enableCI'); +const forcePreloadBundle = config.get('forcePreloadBundle'); const preventDisplaySleepService = new PreventDisplaySleepService( powerSaveBlocker @@ -458,6 +459,9 @@ if (OS.isWindows()) { } async function createWindow() { + const usePreloadBundle = + !isTestEnvironment(getEnvironment()) || forcePreloadBundle; + const windowOptions: Electron.BrowserWindowConstructorOptions = { show: false, width: DEFAULT_WIDTH, @@ -480,9 +484,7 @@ async function createWindow() { contextIsolation: false, preload: join( __dirname, - isTestEnvironment(getEnvironment()) - ? '../preload.js' - : '../preload.bundle.js' + usePreloadBundle ? '../preload.bundle.js' : '../preload.js' ), nativeWindowOpen: true, spellcheck: await getSpellCheckSetting(), diff --git a/ci.js b/ci.js new file mode 100644 index 000000000..44dc16df6 --- /dev/null +++ b/ci.js @@ -0,0 +1,8 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +const config = require('./app/config').default; + +config.util.extendDeep(config, JSON.parse(process.env.SIGNAL_CI_CONFIG || '')); + +require('./app/main'); diff --git a/config/default.json b/config/default.json index 59f817f26..99b6cc328 100644 --- a/config/default.json +++ b/config/default.json @@ -17,6 +17,7 @@ "sfuUrl": "https://sfu.voip.signal.org/", "updatesEnabled": false, "enableCI": false, + "forcePreloadBundle": false, "openDevTools": false, "buildCreation": 0, "buildExpiration": 0, diff --git a/package.json b/package.json index 21f7847ff..80f602b29 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "test-electron": "node ts/scripts/test-electron.js", "test-release": "node ts/scripts/test-release.js", "test-node": "electron-mocha --file test/setup-test-node.js --recursive test/modules ts/test-node ts/test-both", + "test-mock": "mocha ts/test-mock/**/*_test.js", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/modules ts/test-node ts/test-both", "eslint": "eslint --cache .", "lint": "yarn format --list-different && yarn eslint", @@ -185,6 +186,7 @@ "@babel/preset-typescript": "7.16.0", "@chanzuckerberg/axe-storybook-testing": "3.0.2", "@electron/fuses": "1.5.0", + "@signalapp/mock-server": "1.0.1", "@storybook/addon-actions": "5.1.11", "@storybook/addon-knobs": "5.1.11", "@storybook/addons": "5.1.11", @@ -197,6 +199,7 @@ "@types/classnames": "2.2.3", "@types/config": "0.0.39", "@types/dashdash": "1.14.0", + "@types/debug": "4.1.7", "@types/filesize": "3.6.0", "@types/fs-extra": "5.0.5", "@types/google-libphonenumber": "7.4.23", @@ -255,6 +258,7 @@ "core-js": "2.6.9", "cross-env": "5.2.0", "css-loader": "3.2.0", + "debug": "4.3.3", "electron": "16.0.8", "electron-builder": "22.14.5", "electron-mocha": "11.0.2", diff --git a/ts/test-mock/benchmarks/convo_open_bench.ts b/ts/test-mock/benchmarks/convo_open_bench.ts new file mode 100644 index 000000000..1c4bf7776 --- /dev/null +++ b/ts/test-mock/benchmarks/convo_open_bench.ts @@ -0,0 +1,101 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop, no-console */ + +import type { PrimaryDevice } from '@signalapp/mock-server'; + +import { Bootstrap, debug, stats, RUN_COUNT, DISCARD_COUNT } from './fixtures'; + +const CONVERSATION_SIZE = 1000; // messages +const DELAY = 50; // milliseconds + +(async () => { + const bootstrap = new Bootstrap({ + benchmark: true, + }); + + await bootstrap.init(); + const app = await bootstrap.link(); + + try { + const { server, contacts, phone, desktop } = bootstrap; + + const [first, second] = contacts; + + const messages = new Array(); + debug('encrypting'); + // Send messages from just two contacts + for (const contact of [second, first]) { + for (let i = 0; i < CONVERSATION_SIZE; i += 1) { + const messageTimestamp = bootstrap.getTimestamp(); + messages.push( + await contact.encryptText( + desktop, + `hello from: ${contact.profileName}`, + { + timestamp: messageTimestamp, + sealed: true, + } + ) + ); + + messages.push( + await phone.encryptSyncRead(desktop, { + timestamp: bootstrap.getTimestamp(), + messages: [ + { + senderUUID: contact.device.uuid, + timestamp: messageTimestamp, + }, + ], + }) + ); + } + } + + const sendQueue = async (): Promise => { + await Promise.all(messages.map(message => server.send(desktop, message))); + }; + + const measure = async (): Promise => { + const window = await app.getWindow(); + + const leftPane = window.locator('.left-pane-wrapper'); + + const openConvo = async (contact: PrimaryDevice): Promise => { + debug('opening conversation', contact.profileName); + const item = leftPane.locator( + '_react=BaseConversationListItem' + + `[title = ${JSON.stringify(contact.profileName)}]` + ); + + await item.click(); + }; + + const deltaList = new Array(); + for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) { + await openConvo(runId % 2 === 0 ? first : second); + + debug('waiting for timing from the app'); + const { delta } = await app.waitForConversationOpen(); + + // Let render complete + await new Promise(resolve => setTimeout(resolve, DELAY)); + + if (runId >= DISCARD_COUNT) { + deltaList.push(delta); + console.log('run=%d info=%j', runId - DISCARD_COUNT, { delta }); + } else { + console.log('discarded=%d info=%j', runId, { delta }); + } + } + + console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) }); + }; + + await Promise.all([sendQueue(), measure()]); + } finally { + await app.close(); + await bootstrap.teardown(); + } +})(); diff --git a/ts/test-mock/benchmarks/fixtures.ts b/ts/test-mock/benchmarks/fixtures.ts new file mode 100644 index 000000000..48c1c0384 --- /dev/null +++ b/ts/test-mock/benchmarks/fixtures.ts @@ -0,0 +1,67 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop, no-console */ + +import createDebug from 'debug'; + +export const debug = createDebug('mock:benchmarks'); + +export { Bootstrap } from '../bootstrap'; +export { App } from '../playwright'; + +export type StatsType = { + mean: number; + stddev: number; + [key: string]: number; +}; + +export const RUN_COUNT = process.env.RUN_COUNT + ? parseInt(process.env.RUN_COUNT, 10) + : 100; + +export const GROUP_SIZE = process.env.GROUP_SIZE + ? parseInt(process.env.GROUP_SIZE, 10) + : 8; + +export const DISCARD_COUNT = process.env.DISCARD_COUNT + ? parseInt(process.env.DISCARD_COUNT, 10) + : 5; + +export function stats( + list: ReadonlyArray, + percentiles: ReadonlyArray = [] +): StatsType { + if (list.length === 0) { + throw new Error('Empty list given to stats'); + } + + let mean = 0; + let stddev = 0; + + for (const value of list) { + mean += value; + stddev += value ** 2; + } + mean /= list.length; + stddev /= list.length; + + stddev -= mean ** 2; + stddev = Math.sqrt(stddev); + + const sorted = list.slice().sort((a, b) => a - b); + + const result: StatsType = { mean, stddev }; + + for (const p of percentiles) { + result[`p${p}`] = sorted[Math.floor((sorted.length * p) / 100)]; + } + + return result; +} + +// Can happen if electron exits prematurely +process.on('unhandledRejection', reason => { + console.error('Unhandled rejection:'); + console.error(reason); + process.exit(1); +}); diff --git a/ts/test-mock/benchmarks/group_send_bench.ts b/ts/test-mock/benchmarks/group_send_bench.ts new file mode 100644 index 000000000..e9c765bd6 --- /dev/null +++ b/ts/test-mock/benchmarks/group_send_bench.ts @@ -0,0 +1,184 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop, no-console */ + +import assert from 'assert'; + +import { + StorageState, + EnvelopeType, + ReceiptType, +} from '@signalapp/mock-server'; +import { + Bootstrap, + debug, + stats, + RUN_COUNT, + GROUP_SIZE, + DISCARD_COUNT, +} from './fixtures'; + +const CONVERSATION_SIZE = 500; // messages +const LAST_MESSAGE = 'start sending messages now'; + +(async () => { + const bootstrap = new Bootstrap({ + benchmark: true, + }); + + await bootstrap.init(); + + const { contacts, phone } = bootstrap; + + const members = [...contacts].slice(0, GROUP_SIZE); + + const group = await phone.createGroup({ + title: 'Mock Group', + members: [phone, ...members], + }); + + await phone.setStorageState( + StorageState.getEmpty() + .addGroup(group, { whitelisted: true }) + .pinGroup(group) + ); + + const app = await bootstrap.link(); + + try { + const { server, desktop } = bootstrap; + const [first] = members; + + const messages = new Array(); + debug('encrypting'); + // Fill left pane + for (const contact of members.slice().reverse()) { + const messageTimestamp = bootstrap.getTimestamp(); + + messages.push( + await contact.encryptText( + desktop, + `hello from: ${contact.profileName}`, + { + timestamp: messageTimestamp, + sealed: true, + } + ) + ); + messages.push( + await phone.encryptSyncRead(desktop, { + timestamp: bootstrap.getTimestamp(), + messages: [ + { + senderUUID: contact.device.uuid, + timestamp: messageTimestamp, + }, + ], + }) + ); + } + + // Fill group + for (let i = 0; i < CONVERSATION_SIZE; i += 1) { + const contact = members[i % members.length]; + const messageTimestamp = bootstrap.getTimestamp(); + + const isLast = i === CONVERSATION_SIZE - 1; + + messages.push( + await contact.encryptText( + desktop, + isLast ? LAST_MESSAGE : `#${i} from: ${contact.profileName}`, + { + timestamp: messageTimestamp, + sealed: true, + group, + } + ) + ); + messages.push( + await phone.encryptSyncRead(desktop, { + timestamp: bootstrap.getTimestamp(), + messages: [ + { + senderUUID: contact.device.uuid, + timestamp: messageTimestamp, + }, + ], + }) + ); + } + debug('encrypted'); + + await Promise.all(messages.map(message => server.send(desktop, message))); + + const window = await app.getWindow(); + + debug('opening conversation'); + { + const leftPane = window.locator('.left-pane-wrapper'); + + const item = leftPane.locator( + '_react=BaseConversationListItem' + + `[title = ${JSON.stringify(group.title)}]` + + `>> ${JSON.stringify(LAST_MESSAGE)}` + ); + await item.click(); + } + + const timeline = window.locator( + '.timeline-wrapper, .ConversationView__template .react-wrapper' + ); + + const deltaList = new Array(); + for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) { + debug('finding composition input and clicking it'); + const composeArea = window.locator( + '.composition-area-wrapper, ' + + '.ConversationView__template .react-wrapper' + ); + + const input = composeArea.locator('_react=CompositionInput'); + + debug('entering message text'); + await input.type(`my message ${runId}`); + await input.press('Enter'); + + debug('waiting for message on server side'); + const { body, source, envelopeType } = await first.waitForMessage(); + assert.strictEqual(body, `my message ${runId}`); + assert.strictEqual(source, desktop); + assert.strictEqual(envelopeType, EnvelopeType.SenderKey); + + debug('waiting for timing from the app'); + const { timestamp, delta } = await app.waitForMessageSend(); + + debug('sending delivery receipts'); + const delivery = await first.encryptReceipt(desktop, { + timestamp: timestamp + 1, + messageTimestamps: [timestamp], + type: ReceiptType.Delivery, + }); + + await server.send(desktop, delivery); + + debug('waiting for message state change'); + const message = timeline.locator( + `_react=Message[timestamp = ${timestamp}][status = "delivered"]` + ); + await message.waitFor(); + + if (runId >= DISCARD_COUNT) { + deltaList.push(delta); + console.log('run=%d info=%j', runId - DISCARD_COUNT, { delta }); + } else { + console.log('discarded=%d info=%j', runId, { delta }); + } + } + + console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) }); + } finally { + await app.close(); + await bootstrap.teardown(); + } +})(); diff --git a/ts/test-mock/benchmarks/send_bench.ts b/ts/test-mock/benchmarks/send_bench.ts new file mode 100644 index 000000000..14c39ca14 --- /dev/null +++ b/ts/test-mock/benchmarks/send_bench.ts @@ -0,0 +1,135 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop, no-console */ + +import assert from 'assert'; + +import { ReceiptType } from '@signalapp/mock-server'; + +import { Bootstrap, debug, stats, RUN_COUNT, DISCARD_COUNT } from './fixtures'; + +const CONVERSATION_SIZE = 500; // messages + +const LAST_MESSAGE = 'start sending messages now'; + +(async () => { + const bootstrap = new Bootstrap({ + benchmark: true, + }); + + await bootstrap.init(); + const app = await bootstrap.link(); + + try { + const { server, contacts, phone, desktop } = bootstrap; + + const [first] = contacts; + + const messages = new Array(); + debug('encrypting'); + // Note: make it so that we receive the latest message from the first + // contact. + for (const contact of contacts.slice().reverse()) { + let count = 1; + if (contact === first) { + count = CONVERSATION_SIZE; + } + + for (let i = 0; i < count; i += 1) { + const messageTimestamp = bootstrap.getTimestamp(); + + const isLast = i === count - 1; + + messages.push( + await contact.encryptText( + desktop, + isLast ? LAST_MESSAGE : `#${i} from: ${contact.profileName}`, + { + timestamp: messageTimestamp, + sealed: true, + } + ) + ); + messages.push( + await phone.encryptSyncRead(desktop, { + timestamp: bootstrap.getTimestamp(), + messages: [ + { + senderUUID: contact.device.uuid, + timestamp: messageTimestamp, + }, + ], + }) + ); + } + } + + await Promise.all(messages.map(message => server.send(desktop, message))); + + const window = await app.getWindow(); + + debug('opening conversation'); + { + const leftPane = window.locator('.left-pane-wrapper'); + const item = leftPane.locator( + '_react=BaseConversationListItem' + + `[title = ${JSON.stringify(first.profileName)}]` + + `>> ${JSON.stringify(LAST_MESSAGE)}` + ); + await item.click(); + } + + const timeline = window.locator( + '.timeline-wrapper, .ConversationView__template .react-wrapper' + ); + + const deltaList = new Array(); + for (let runId = 0; runId < RUN_COUNT + DISCARD_COUNT; runId += 1) { + debug('finding composition input and clicking it'); + const composeArea = window.locator( + '.composition-area-wrapper, ' + + '.ConversationView__template .react-wrapper' + ); + const input = composeArea.locator('_react=CompositionInput'); + + debug('entering message text'); + await input.type(`my message ${runId}`); + await input.press('Enter'); + + debug('waiting for message on server side'); + const { body, source } = await first.waitForMessage(); + assert.strictEqual(body, `my message ${runId}`); + assert.strictEqual(source, desktop); + + debug('waiting for timing from the app'); + const { timestamp, delta } = await app.waitForMessageSend(); + + debug('sending delivery receipt'); + const delivery = await first.encryptReceipt(desktop, { + timestamp: timestamp + 1, + messageTimestamps: [timestamp], + type: ReceiptType.Delivery, + }); + + await server.send(desktop, delivery); + + debug('waiting for message state change'); + const message = timeline.locator( + `_react=Message[timestamp = ${timestamp}][status = "delivered"]` + ); + await message.waitFor(); + + if (runId >= DISCARD_COUNT) { + deltaList.push(delta); + console.log('run=%d info=%j', runId - DISCARD_COUNT, { delta }); + } else { + console.log('discarded=%d info=%j', runId, { delta }); + } + } + + console.log('stats info=%j', { delta: stats(deltaList, [99, 99.8]) }); + } finally { + await app.close(); + await bootstrap.teardown(); + } +})(); diff --git a/ts/test-mock/benchmarks/startup_bench.ts b/ts/test-mock/benchmarks/startup_bench.ts new file mode 100644 index 000000000..2f0b4e123 --- /dev/null +++ b/ts/test-mock/benchmarks/startup_bench.ts @@ -0,0 +1,133 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop, no-console */ + +import { ReceiptType } from '@signalapp/mock-server'; + +import { debug, Bootstrap, stats, RUN_COUNT } from './fixtures'; + +const MESSAGE_BATCH_SIZE = 1000; // messages + +const ENABLE_RECEIPTS = Boolean(process.env.ENABLE_RECEIPTS); + +(async () => { + const bootstrap = new Bootstrap({ + benchmark: true, + }); + + await bootstrap.init(); + await bootstrap.linkAndClose(); + + try { + const { server, contacts, phone, desktop } = bootstrap; + + const messagesPerSec = new Array(); + + for (let runId = 0; runId < RUN_COUNT; runId += 1) { + // Generate messages + const messagePromises = new Array>(); + debug('started generating messages'); + + for (let i = 0; i < MESSAGE_BATCH_SIZE; i += 1) { + const contact = contacts[Math.floor(i / 2) % contacts.length]; + const direction = i % 2 ? 'message' : 'reply'; + + const messageTimestamp = bootstrap.getTimestamp(); + + if (direction === 'message') { + messagePromises.push( + contact.encryptText( + desktop, + `Ping from mock server ${i + 1} / ${MESSAGE_BATCH_SIZE}`, + { + timestamp: messageTimestamp, + sealed: true, + } + ) + ); + + if (ENABLE_RECEIPTS) { + messagePromises.push( + phone.encryptSyncRead(desktop, { + timestamp: bootstrap.getTimestamp(), + messages: [ + { + senderUUID: contact.device.uuid, + timestamp: messageTimestamp, + }, + ], + }) + ); + } + continue; + } + + messagePromises.push( + phone.encryptSyncSent( + desktop, + `Pong from mock server ${i + 1} / ${MESSAGE_BATCH_SIZE}`, + { + timestamp: messageTimestamp, + destinationUUID: contact.device.uuid, + } + ) + ); + + if (ENABLE_RECEIPTS) { + messagePromises.push( + contact.encryptReceipt(desktop, { + timestamp: bootstrap.getTimestamp(), + messageTimestamps: [messageTimestamp], + type: ReceiptType.Delivery, + }) + ); + messagePromises.push( + contact.encryptReceipt(desktop, { + timestamp: bootstrap.getTimestamp(), + messageTimestamps: [messageTimestamp], + type: ReceiptType.Read, + }) + ); + } + } + + debug('ended generating messages'); + + const messages = await Promise.all(messagePromises); + + // Open the flood gates + { + debug('got synced, sending messages'); + + // Queue all messages + const queue = async (): Promise => { + await Promise.all( + messages.map(message => { + return server.send(desktop, message); + }) + ); + }; + + const run = async (): Promise => { + const app = await bootstrap.startApp(); + const appLoadedInfo = await app.waitUntilLoaded(); + + console.log('run=%d info=%j', runId, appLoadedInfo); + + messagesPerSec.push(appLoadedInfo.messagesPerSec); + + await app.close(); + }; + + await Promise.all([queue(), run()]); + } + } + + // Compute human-readable statistics + if (messagesPerSec.length !== 0) { + console.log('stats info=%j', { messagesPerSec: stats(messagesPerSec) }); + } + } finally { + await bootstrap.teardown(); + } +})(); diff --git a/ts/test-mock/bootstrap.ts b/ts/test-mock/bootstrap.ts new file mode 100644 index 000000000..07f28d0d2 --- /dev/null +++ b/ts/test-mock/bootstrap.ts @@ -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(); +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; + benchmark?: boolean; + + linkedDevices?: number; + contactCount?: number; +}>; + +type BootstrapInternalOptions = Pick & + 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; + 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 { + 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 { + 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 { + 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 { + const app = await this.link(); + + debug('closing the app after link'); + await app.close(); + } + + public async startApp(): Promise { + 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 { + assert( + this.privContacts, + 'Bootstrap has to be initialized first, see: bootstrap.init()' + ); + return this.privContacts; + } + + // + // Private + // + + private async generateConfig(port: number): Promise { + 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, + }); + } +} diff --git a/ts/test-mock/playwright.ts b/ts/test-mock/playwright.ts new file mode 100644 index 000000000..c991f4214 --- /dev/null +++ b/ts/test-mock/playwright.ts @@ -0,0 +1,85 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ElectronApplication, Page } from 'playwright'; +import { _electron as electron } from 'playwright'; + +export type AppLoadedInfoType = Readonly<{ + loadTime: number; + messagesPerSec: number; +}>; + +export type MessageSendInfoType = Readonly<{ + timestamp: number; + delta: number; +}>; + +export type ConversationOpenInfoType = Readonly<{ + delta: number; +}>; + +export type AppOptionsType = Readonly<{ + main: string; + args: ReadonlyArray; + config: string; +}>; + +export class App { + private privApp: ElectronApplication | undefined; + + constructor(private readonly options: AppOptionsType) {} + + public async start(): Promise { + this.privApp = await electron.launch({ + executablePath: this.options.main, + args: this.options.args.slice(), + env: { + ...process.env, + SIGNAL_CI_CONFIG: this.options.config, + }, + locale: 'en', + }); + } + + public async waitForProvisionURL(): Promise { + return this.waitForEvent('provisioning-url'); + } + + public async waitUntilLoaded(): Promise { + return this.waitForEvent('app-loaded'); + } + + public async waitForMessageSend(): Promise { + return this.waitForEvent('message:send-complete'); + } + + public async waitForConversationOpen(): Promise { + return this.waitForEvent('conversation:open'); + } + + public async close(): Promise { + await this.app.close(); + } + + public async getWindow(): Promise { + return this.app.firstWindow(); + } + + private async waitForEvent(event: string): Promise { + const window = await this.getWindow(); + + const result = await window.evaluate( + `window.CI.waitForEvent(${JSON.stringify(event)})` + ); + + return result as T; + } + + private get app(): ElectronApplication { + if (!this.privApp) { + throw new Error('Call ElectronWrap.start() first'); + } + + return this.privApp; + } +} diff --git a/ts/test-mock/storage/archive_test.ts b/ts/test-mock/storage/archive_test.ts new file mode 100644 index 000000000..435d0689b --- /dev/null +++ b/ts/test-mock/storage/archive_test.ts @@ -0,0 +1,123 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import * as durations from '../../util/durations'; +import type { App, Bootstrap } from './fixtures'; +import { initStorage, debug } from './fixtures'; + +describe('storage service', function needsName() { + this.timeout(durations.MINUTE); + + let bootstrap: Bootstrap; + let app: App; + + beforeEach(async () => { + ({ bootstrap, app } = await initStorage()); + }); + + afterEach(async () => { + await app.close(); + await bootstrap.teardown(); + }); + + it('should archive/unarchive contacts', async () => { + const { phone, contacts } = bootstrap; + const [firstContact] = contacts; + + const window = await app.getWindow(); + + const leftPane = window.locator('.left-pane-wrapper'); + const conversationStack = window.locator('.conversation-stack'); + + debug('archiving contact'); + { + const state = await phone.expectStorageState('consistency check'); + + await phone.setStorageState( + state + .updateContact(firstContact, { archived: true }) + .unpin(firstContact) + ); + await phone.sendFetchStorage({ + timestamp: bootstrap.getTimestamp(), + }); + + await leftPane + .locator( + '_react=ConversationListItem' + + `[title = ${JSON.stringify(firstContact.profileName)}]` + ) + .waitFor({ state: 'hidden' }); + + await leftPane + .locator('button.module-conversation-list__item--archive-button') + .waitFor(); + } + + debug('unarchiving pinned contact'); + { + const state = await phone.expectStorageState('consistency check'); + + await phone.setStorageState( + state.updateContact(firstContact, { archived: false }).pin(firstContact) + ); + await phone.sendFetchStorage({ + timestamp: bootstrap.getTimestamp(), + }); + + await leftPane + .locator( + '_react=ConversationListItem' + + '[isPinned = true]' + + `[title = ${JSON.stringify(firstContact.profileName)}]` + ) + .waitFor(); + + await leftPane + .locator('button.module-conversation-list__item--archive-button') + .waitFor({ state: 'hidden' }); + } + + debug('archive pinned contact in the app'); + { + const state = await phone.expectStorageState('consistency check'); + + await leftPane + .locator( + '_react=ConversationListItem' + + `[title = ${JSON.stringify(firstContact.profileName)}]` + ) + .click(); + + const moreButton = conversationStack.locator( + 'button.module-ConversationHeader__button--more' + ); + await moreButton.click(); + + const archiveButton = conversationStack.locator( + '.react-contextmenu-item >> "Archive"' + ); + await archiveButton.click(); + + const newState = await phone.waitForStorageState({ + after: state, + }); + assert.ok(!(await newState.isPinned(firstContact)), 'contact not pinned'); + const record = await newState.getContact(firstContact); + assert.ok(record, 'contact record not found'); + assert.ok(record?.archived, 'contact archived'); + + // AccountRecord + ContactRecord + const { added, removed } = newState.diff(state); + assert.strictEqual(added.length, 2, 'only two records must be added'); + assert.strictEqual(removed.length, 2, 'only two records must be removed'); + } + + debug('Verifying the final manifest version'); + const finalState = await phone.expectStorageState('consistency check'); + + assert.strictEqual(finalState.version, 4); + }); +}); diff --git a/ts/test-mock/storage/fixtures.ts b/ts/test-mock/storage/fixtures.ts new file mode 100644 index 000000000..f01956b37 --- /dev/null +++ b/ts/test-mock/storage/fixtures.ts @@ -0,0 +1,98 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import createDebug from 'debug'; +import type { Group, PrimaryDevice } from '@signalapp/mock-server'; +import { StorageState, Proto } from '@signalapp/mock-server'; +import { App } from '../playwright'; +import { Bootstrap } from '../bootstrap'; + +export const debug = createDebug('mock:test-storage'); + +export { App, Bootstrap }; + +const GROUP_SIZE = 8; + +export type InitStorageResultType = Readonly<{ + bootstrap: Bootstrap; + app: App; + group: Group; + members: Array; +}>; + +// +// This function creates an initial storage service state that includes: +// +// - All contacts from contact sync (first contact pinned) +// - A pinned group with GROUP_SIZE members (from the contacts) +// - Account with e164 and profileKey +// +// In addition to above, this function will queue one incoming message in the +// group, and one for the first contact (so that both will appear in the left +// pane). +export async function initStorage(): Promise { + // Creates primary device, contacts + const bootstrap = new Bootstrap(); + + await bootstrap.init(); + + // Populate storage service + const { contacts, phone } = bootstrap; + + const [firstContact] = contacts; + + const members = [...contacts].slice(0, GROUP_SIZE); + + const group = await phone.createGroup({ + title: 'Mock Group', + members: [phone, ...members], + }); + + let state = StorageState.getEmpty(); + + state = state.updateAccount({ + profileKey: phone.profileKey.serialize(), + e164: phone.device.number, + }); + + state = state + .addGroup(group, { + whitelisted: true, + }) + .pinGroup(group); + + for (const contact of contacts) { + state = state.addContact(contact, { + identityState: Proto.ContactRecord.IdentityState.VERIFIED, + whitelisted: true, + + identityKey: contact.publicKey.serialize(), + profileKey: contact.profileKey.serialize(), + }); + } + + state = state.pin(firstContact); + + await phone.setStorageState(state); + + // Link new device + const app = await bootstrap.link(); + + const { desktop } = bootstrap; + + // Send a message to the group and the first contact + const contactSend = contacts[0].sendText(desktop, 'hello from contact', { + timestamp: bootstrap.getTimestamp(), + sealed: true, + }); + + const groupSend = members[0].sendText(desktop, 'hello in group', { + timestamp: bootstrap.getTimestamp(), + sealed: true, + group, + }); + + await Promise.all([contactSend, groupSend]); + + return { bootstrap, app, group, members }; +} diff --git a/ts/test-mock/storage/message_request_test.ts b/ts/test-mock/storage/message_request_test.ts new file mode 100644 index 000000000..09373cd54 --- /dev/null +++ b/ts/test-mock/storage/message_request_test.ts @@ -0,0 +1,115 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import * as durations from '../../util/durations'; +import type { App, Bootstrap } from './fixtures'; +import { initStorage, debug } from './fixtures'; + +describe('storage service', function needsName() { + this.timeout(durations.MINUTE); + + let bootstrap: Bootstrap; + let app: App; + + beforeEach(async () => { + ({ bootstrap, app } = await initStorage()); + }); + + afterEach(async () => { + await app.close(); + await bootstrap.teardown(); + }); + + it('should handle message request state changes', async () => { + const { phone, desktop, server } = bootstrap; + + debug('Creating stranger'); + const stranger = await server.createPrimaryDevice({ + profileName: 'Mysterious Stranger', + }); + + const ourKey = await desktop.popSingleUseKey(); + await stranger.addSingleUseKey(desktop, ourKey); + + debug('Sending a message from a stranger'); + await stranger.sendText(desktop, 'Hello!', { + withProfileKey: true, + timestamp: bootstrap.getTimestamp(), + }); + + const window = await app.getWindow(); + + const leftPane = window.locator('.left-pane-wrapper'); + const conversationStack = window.locator('.conversation-stack'); + + debug('Opening conversation with a stranger'); + await leftPane + .locator( + '_react=ConversationListItem' + + `[title = ${JSON.stringify(stranger.profileName)}]` + ) + .click(); + + const initialState = await phone.expectStorageState('initial state'); + assert.strictEqual(initialState.version, 1); + assert.isUndefined(initialState.getContact(stranger)); + + debug('Accept conversation from a stranger'); + await conversationStack + .locator('.module-message-request-actions button >> "Accept"') + .click(); + + debug('Verify that storage state was updated'); + { + const nextState = await phone.waitForStorageState({ + after: initialState, + }); + assert.strictEqual(nextState.version, 2); + assert.isTrue(nextState.getContact(stranger)?.whitelisted); + + // ContactRecord + const { added, removed } = nextState.diff(initialState); + assert.strictEqual(added.length, 1, 'only one record must be added'); + assert.strictEqual(removed.length, 0, 'no records should be removed'); + } + + // Stranger should receive our profile key + { + const { body, source, dataMessage } = await stranger.waitForMessage(); + assert.strictEqual(body, '', 'profile key message has no body'); + assert.strictEqual( + source, + desktop, + 'profile key message has valid source' + ); + assert.isTrue( + phone.profileKey + .serialize() + .equals(dataMessage.profileKey ?? new Uint8Array(0)), + 'profile key message has correct profile key' + ); + } + + debug('Enter message text'); + const composeArea = window.locator( + '.composition-area-wrapper, ' + + '.ConversationView__template .react-wrapper' + ); + const input = composeArea.locator('_react=CompositionInput'); + + await input.type('hello stranger!'); + await input.press('Enter'); + + { + const { body, source } = await stranger.waitForMessage(); + assert.strictEqual(body, 'hello stranger!', 'text message has body'); + assert.strictEqual(source, desktop, 'text message has valid source'); + } + + debug('Verifying the final manifest version'); + const finalState = await phone.expectStorageState('consistency check'); + assert.strictEqual(finalState.version, 2); + }); +}); diff --git a/ts/test-mock/storage/pin_unpin_test.ts b/ts/test-mock/storage/pin_unpin_test.ts new file mode 100644 index 000000000..73047b22a --- /dev/null +++ b/ts/test-mock/storage/pin_unpin_test.ts @@ -0,0 +1,160 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-await-in-loop */ + +import { assert } from 'chai'; + +import type { Group } from '@signalapp/mock-server'; + +import * as durations from '../../util/durations'; +import type { App, Bootstrap } from './fixtures'; +import { initStorage, debug } from './fixtures'; + +describe('storage service', function needsName() { + this.timeout(durations.MINUTE); + + let bootstrap: Bootstrap; + let app: App; + let group: Group; + + beforeEach(async () => { + ({ bootstrap, app, group } = await initStorage()); + }); + + afterEach(async () => { + await app.close(); + await bootstrap.teardown(); + }); + + it('should pin/unpin groups', async () => { + const { phone, desktop, contacts } = bootstrap; + + const window = await app.getWindow(); + + const leftPane = window.locator('.left-pane-wrapper'); + const conversationStack = window.locator('.conversation-stack'); + + debug('Verifying that the group is pinned on startup'); + await leftPane + .locator( + '_react=ConversationListItem' + + '[isPinned = true] ' + + `[title = ${JSON.stringify(group.title)}]` + ) + .waitFor(); + + debug('Unpinning group via storage service'); + { + const state = await phone.expectStorageState('initial state'); + + await phone.setStorageState(state.unpinGroup(group)); + await phone.sendFetchStorage({ + timestamp: bootstrap.getTimestamp(), + }); + + await leftPane + .locator( + '_react=ConversationListItem' + + '[isPinned = false] ' + + `[title = ${JSON.stringify(group.title)}]` + ) + .waitFor(); + } + + debug('Pinning group in the app'); + { + const state = await phone.expectStorageState('consistency check'); + + const convo = leftPane.locator( + '_react=ConversationListItem' + + '[isPinned = false] ' + + `[title = ${JSON.stringify(group.title)}]` + ); + await convo.click(); + + const moreButton = conversationStack.locator( + 'button.module-ConversationHeader__button--more' + ); + await moreButton.click(); + + const pinButton = conversationStack.locator( + '.react-contextmenu-item >> "Pin Conversation"' + ); + await pinButton.click(); + + const newState = await phone.waitForStorageState({ + after: state, + }); + assert.isTrue(await newState.isGroupPinned(group), 'group not pinned'); + + // AccountRecord + const { added, removed } = newState.diff(state); + assert.strictEqual(added.length, 1, 'only one record must be added'); + assert.strictEqual(removed.length, 1, 'only one record must be removed'); + } + + debug('Pinning > 4 conversations'); + { + // We already have one group and first contact pinned so we need three + // more. + const toPin = contacts.slice(1, 4); + + // To do that we need them to appear in the left pane, though. + for (const [i, contact] of toPin.entries()) { + const isLast = i === toPin.length - 1; + + debug('sending a message to contact=%d', i); + await contact.sendText(desktop, 'Hello!', { + timestamp: bootstrap.getTimestamp(), + }); + + const state = await phone.expectStorageState('consistency check'); + + debug('pinning contact=%d', i); + const convo = leftPane.locator( + '_react=ConversationListItem' + + `[title = ${JSON.stringify(contact.profileName)}]` + ); + await convo.click(); + + const moreButton = conversationStack.locator( + 'button.module-ConversationHeader__button--more' + ); + await moreButton.click(); + + const pinButton = conversationStack.locator( + '.react-contextmenu-item >> "Pin Conversation"' + ); + await pinButton.click(); + + if (isLast) { + // Storage state shouldn't be updated because we failed to pin + await window + .locator('.Toast >> "You can only pin up to 4 chats"') + .waitFor(); + break; + } + + debug('verifying storage state change contact=%d', i); + const newState = await phone.waitForStorageState({ + after: state, + }); + assert.isTrue(await newState.isPinned(contact), 'contact not pinned'); + + // AccountRecord + const { added, removed } = newState.diff(state); + assert.strictEqual(added.length, 1, 'only one record must be added'); + assert.strictEqual( + removed.length, + 1, + 'only one record must be removed' + ); + } + } + + debug('Verifying the final manifest version'); + const finalState = await phone.expectStorageState('consistency check'); + + assert.strictEqual(finalState.version, 5); + }); +}); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 138e8ed2c..1f83415dc 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -167,6 +167,27 @@ "reasonCategory": "falseMatch", "updated": "2022-01-27T20:06:59.988Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/@malept/flatpak-bundler/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/@malept/flatpak-bundler/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/@malept/flatpak-bundler/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "eval", "path": "node_modules/@protobufjs/inquire/index.js", @@ -595,6 +616,28 @@ "updated": "2020-08-28T16:12:19.904Z", "reasonDetail": "isn't jquery" }, + { + "rule": "jQuery-load(", + "path": "node_modules/agent-base/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/agent-base/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted", + "updated": "2022-02-11T21:58:24.827Z", + "reasonDetail": "" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/agent-base/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-wrap(", "path": "node_modules/asar/node_modules/commander/index.js", @@ -1255,6 +1298,27 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-notarize/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-notarize/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/electron-notarize/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-append(", "path": "node_modules/enquirer/lib/prompts/autocomplete.js", @@ -1544,6 +1608,27 @@ "reasonCategory": "falseMatch", "updated": "2020-09-11T17:24:56.124Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/get-uri/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/get-uri/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/get-uri/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-$(", "path": "node_modules/global-dirs/index.js", @@ -1670,6 +1755,48 @@ "reasonCategory": "falseMatch", "updated": "2021-11-13T01:38:33.299Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/http-proxy-agent/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/http-proxy-agent/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/http-proxy-agent/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/https-proxy-agent/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/https-proxy-agent/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/https-proxy-agent/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-$(", "path": "node_modules/immutable/dist/immutable.min.js", @@ -4303,6 +4430,13 @@ "reasonCategory": "falseMatch", "updated": "2019-03-09T00:08:44.242Z" }, + { + "rule": "eval", + "path": "node_modules/micro/node_modules/depd/index.js", + "line": " var deprecatedfn = eval('(function (' + args + ') {\\n' +", + "reasonCategory": "usageTrusted", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "DOM-innerHTML", "path": "node_modules/min-document/serialize.js", @@ -4469,6 +4603,27 @@ "reasonCategory": "falseMatch", "updated": "2020-09-14T16:19:54.461Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/pac-proxy-agent/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/pac-proxy-agent/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/pac-proxy-agent/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "DOM-innerHTML", "path": "node_modules/package-json/node_modules/@sindresorhus/is/dist/index.js", @@ -5138,6 +5293,27 @@ "reasonCategory": "falseMatch", "updated": "2018-09-15T00:38:04.183Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/proxy-agent/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/proxy-agent/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/proxy-agent/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "DOM-document.write(", "path": "node_modules/qrcode-generator/sample.js", @@ -6292,6 +6468,27 @@ "reasonCategory": "falseMatch", "updated": "2018-09-15T00:38:04.183Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/socks-proxy-agent/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/socks-proxy-agent/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/socks-proxy-agent/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-append(", "path": "node_modules/socks/build/client/socksclient.js", @@ -6395,6 +6592,27 @@ "reasonCategory": "falseMatch", "updated": "2020-04-25T01:47:02.583Z" }, + { + "rule": "jQuery-load(", + "path": "node_modules/sumchecker/node_modules/debug/src/browser.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/sumchecker/node_modules/debug/src/common.js", + "line": "\tcreateDebug.enable(createDebug.load());", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, + { + "rule": "jQuery-load(", + "path": "node_modules/sumchecker/node_modules/debug/src/node.js", + "line": "function load() {", + "reasonCategory": "falseMatch", + "updated": "2022-02-11T21:58:24.827Z" + }, { "rule": "jQuery-append(", "path": "node_modules/table/dist/createStream.js", diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index c59910236..55384d42c 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -111,6 +111,7 @@ const excludedFilesRegexp = RegExp( '^node_modules/esbuild/.+', '^node_modules/@babel/.+', '^node_modules/@chanzuckerberg/axe-storybook-testing/.+', + '^node_modules/@signalapp/mock-server/.+', '^node_modules/@svgr/.+', '^node_modules/@types/.+', '^node_modules/@webassemblyjs/.+', diff --git a/yarn.lock b/yarn.lock index 8f7743be2..f4098102b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1342,6 +1342,30 @@ "@react-spring/shared" "~9.4.0" "@react-spring/types" "~9.4.0" +"@signalapp/mock-server@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-1.0.1.tgz#ae461528ca18218cf34366d5afa1c672b0ddabe0" + integrity sha512-9XYIFZwwGnFEg/WSffn3KWOHHe/ooL44+UQ3cFX68jEtgOk575EeRZaTFge+XNxzciAbDuCtkWivYCODPBJISA== + dependencies: + "@signalapp/signal-client" "0.12.1" + debug "^4.3.2" + long "^4.0.0" + micro "^9.3.4" + microrouter "^3.1.3" + protobufjs "^6.10.2" + typescript "^4.5.5" + url-pattern "^1.0.3" + uuid "^8.3.2" + ws "^8.4.2" + +"@signalapp/signal-client@0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.12.1.tgz#d587811e76308e53376f14fc294f8d0c0af39d91" + integrity sha512-45BJHLVvCU1BMzLL4ZRFnJ5xGUwryozstwpw/VpEDD0Asb5WoZA+G42/Urnr0TbIWg+LYBwEpc7cKZ48SgOodQ== + dependencies: + node-gyp-build "^4.2.3" + uuid "^8.3.0" + "@signalapp/signal-client@0.12.4": version "0.12.4" resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.12.4.tgz#19023456c9249db6afb01762b1841e18cc3614be" @@ -1933,7 +1957,7 @@ resolved "https://registry.yarnpkg.com/@types/dashdash/-/dashdash-1.14.0.tgz#bfa457c2688497cf0e6695dbd522c67a9232833f" integrity sha512-dBnfu9H6TVawx85FGmVEs5lYFXNwUVxn3Nqu5FHhCAi4aPvZR35W4FEMK3ljlpM2vHPGgEnCZGARF59/QGTNJw== -"@types/debug@^4.1.6": +"@types/debug@4.1.7", "@types/debug@^4.1.6": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== @@ -2174,6 +2198,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== +"@types/node@>=13.7.0": + version "17.0.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.17.tgz#a8ddf6e0c2341718d74ee3dc413a13a042c45a0c" + integrity sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw== + "@types/node@^13.7.0": version "13.13.41" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.41.tgz#045a4981318d31a581650ce70f340a32c3461198" @@ -3210,6 +3239,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + arg@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" @@ -5031,7 +5065,7 @@ content-disposition@0.5.3: dependencies: safe-buffer "5.1.2" -content-type@~1.0.4: +content-type@1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -5431,6 +5465,13 @@ debug@4, debug@4.3.2, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, de dependencies: ms "2.1.2" +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -5579,6 +5620,11 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -8216,6 +8262,16 @@ http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + http-errors@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" @@ -8338,6 +8394,11 @@ iconv-corefoundation@^1.1.6: cli-truncate "^1.1.0" node-addon-api "^1.6.3" +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8985,7 +9046,7 @@ is-shared-array-buffer@^1.0.1: resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== -is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@1.1.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -9866,6 +9927,16 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" +micro@^9.3.4: + version "9.3.4" + resolved "https://registry.yarnpkg.com/micro/-/micro-9.3.4.tgz#745a494e53c8916f64fb6a729f8cbf2a506b35ad" + integrity sha512-smz9naZwTG7qaFnEZ2vn248YZq9XR+XoOH3auieZbkhDL4xLOxiE+KqG8qqnBeKfXA9c1uEFGCxPN1D+nT6N7w== + dependencies: + arg "4.1.0" + content-type "1.0.4" + is-stream "1.1.0" + raw-body "2.3.2" + microevent.ts@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" @@ -9915,6 +9986,13 @@ micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +microrouter@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/microrouter/-/microrouter-3.1.3.tgz#1e45df77d3e2d773be5da129cfc7d5e6e6c86f4e" + integrity sha1-HkXfd9Pi13O+XaEpz8fV5ubIb04= + dependencies: + url-pattern "^1.0.3" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -11735,6 +11813,25 @@ protobufjs@6.10.2: "@types/node" "^13.7.0" long "^4.0.0" +protobufjs@^6.10.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" @@ -11962,6 +12059,16 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + raw-body@2.4.0, raw-body@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" @@ -13251,6 +13358,11 @@ setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ= + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -13737,7 +13849,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -14558,6 +14670,11 @@ typescript@4.4.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86" integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ== +typescript@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" @@ -14737,6 +14854,11 @@ url-parse@^1.4.3, url-parse@^1.5.1: querystringify "^2.1.1" requires-port "^1.0.0" +url-pattern@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1" + integrity sha1-BAkpJHGyTyPFDWWkeTF5PStaz8E= + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -14798,7 +14920,7 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -15254,6 +15376,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== +ws@^8.4.2: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"