Test rate-limiting, stories in mock server
This commit is contained in:
parent
450051e541
commit
f9453c64dd
|
@ -193,7 +193,7 @@
|
||||||
"@babel/preset-typescript": "7.17.12",
|
"@babel/preset-typescript": "7.17.12",
|
||||||
"@electron/fuses": "1.5.0",
|
"@electron/fuses": "1.5.0",
|
||||||
"@mixer/parallel-prettier": "2.0.1",
|
"@mixer/parallel-prettier": "2.0.1",
|
||||||
"@signalapp/mock-server": "2.8.1",
|
"@signalapp/mock-server": "2.9.0",
|
||||||
"@storybook/addon-a11y": "6.5.6",
|
"@storybook/addon-a11y": "6.5.6",
|
||||||
"@storybook/addon-actions": "6.5.6",
|
"@storybook/addon-actions": "6.5.6",
|
||||||
"@storybook/addon-controls": "6.5.6",
|
"@storybook/addon-controls": "6.5.6",
|
||||||
|
|
5
ts/CI.ts
5
ts/CI.ts
|
@ -6,6 +6,7 @@ import { ipcRenderer } from 'electron';
|
||||||
import { explodePromise } from './util/explodePromise';
|
import { explodePromise } from './util/explodePromise';
|
||||||
import { SECOND } from './util/durations';
|
import { SECOND } from './util/durations';
|
||||||
import * as log from './logging/log';
|
import * as log from './logging/log';
|
||||||
|
import type { IPCResponse as ChallengeResponseType } from './challenge';
|
||||||
|
|
||||||
type ResolveType = (data: unknown) => void;
|
type ResolveType = (data: unknown) => void;
|
||||||
|
|
||||||
|
@ -84,4 +85,8 @@ export class CI {
|
||||||
}
|
}
|
||||||
resultList.push(data);
|
resultList.push(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public solveChallenge(response: ChallengeResponseType): void {
|
||||||
|
window.Signal.challengeHandler?.onResponse(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,6 +238,10 @@ export async function startApp(): Promise<void> {
|
||||||
},
|
},
|
||||||
|
|
||||||
requestChallenge(request) {
|
requestChallenge(request) {
|
||||||
|
if (window.CI) {
|
||||||
|
window.CI.handleEvent('challenge', request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.sendChallengeRequest(request);
|
window.sendChallengeRequest(request);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -692,7 +692,7 @@ export const SendStoryModal = ({
|
||||||
)}
|
)}
|
||||||
{page === Page.ChooseGroups && (
|
{page === Page.ChooseGroups && (
|
||||||
<button
|
<button
|
||||||
aria-label="SendStoryModal__ok"
|
aria-label={i18n('SendStoryModal__ok')}
|
||||||
className="SendStoryModal__ok"
|
className="SendStoryModal__ok"
|
||||||
disabled={!chosenGroupIds.size}
|
disabled={!chosenGroupIds.size}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -705,7 +705,7 @@ export const SendStoryModal = ({
|
||||||
)}
|
)}
|
||||||
{page === Page.SendStory && (
|
{page === Page.SendStory && (
|
||||||
<button
|
<button
|
||||||
aria-label="SendStoryModal__send"
|
aria-label={i18n('SendStoryModal__send')}
|
||||||
className="SendStoryModal__send"
|
className="SendStoryModal__send"
|
||||||
disabled={!selectedListIds.size && !selectedGroupIds.size}
|
disabled={!selectedListIds.size && !selectedGroupIds.size}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -1270,6 +1270,7 @@ async function processManifest(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!myStories) {
|
if (!myStories) {
|
||||||
|
log.info(`storageService.process(${version}): creating my stories`);
|
||||||
const storyDistribution: StoryDistributionWithMembersType = {
|
const storyDistribution: StoryDistributionWithMembersType = {
|
||||||
allowsReplies: true,
|
allowsReplies: true,
|
||||||
id: MY_STORIES_ID,
|
id: MY_STORIES_ID,
|
||||||
|
|
|
@ -62,6 +62,7 @@ export type BootstrapOptions = Readonly<{
|
||||||
|
|
||||||
linkedDevices?: number;
|
linkedDevices?: number;
|
||||||
contactCount?: number;
|
contactCount?: number;
|
||||||
|
contactsWithoutProfileKey?: number;
|
||||||
contactNames?: ReadonlyArray<string>;
|
contactNames?: ReadonlyArray<string>;
|
||||||
contactPreKeyCount?: number;
|
contactPreKeyCount?: number;
|
||||||
}>;
|
}>;
|
||||||
|
@ -71,6 +72,7 @@ type BootstrapInternalOptions = Pick<BootstrapOptions, 'extraConfig'> &
|
||||||
benchmark: boolean;
|
benchmark: boolean;
|
||||||
linkedDevices: number;
|
linkedDevices: number;
|
||||||
contactCount: number;
|
contactCount: number;
|
||||||
|
contactsWithoutProfileKey: number;
|
||||||
contactNames: ReadonlyArray<string>;
|
contactNames: ReadonlyArray<string>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
@ -104,6 +106,7 @@ export class Bootstrap {
|
||||||
|
|
||||||
private readonly options: BootstrapInternalOptions;
|
private readonly options: BootstrapInternalOptions;
|
||||||
private privContacts?: ReadonlyArray<PrimaryDevice>;
|
private privContacts?: ReadonlyArray<PrimaryDevice>;
|
||||||
|
private privContactsWithoutProfileKey?: ReadonlyArray<PrimaryDevice>;
|
||||||
private privPhone?: PrimaryDevice;
|
private privPhone?: PrimaryDevice;
|
||||||
private privDesktop?: Device;
|
private privDesktop?: Device;
|
||||||
private storagePath?: string;
|
private storagePath?: string;
|
||||||
|
@ -118,13 +121,17 @@ export class Bootstrap {
|
||||||
this.options = {
|
this.options = {
|
||||||
linkedDevices: 5,
|
linkedDevices: 5,
|
||||||
contactCount: MAX_CONTACTS,
|
contactCount: MAX_CONTACTS,
|
||||||
|
contactsWithoutProfileKey: 0,
|
||||||
contactNames: CONTACT_NAMES,
|
contactNames: CONTACT_NAMES,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert(this.options.contactCount <= this.options.contactNames.length);
|
assert(
|
||||||
|
this.options.contactCount + this.options.contactsWithoutProfileKey <=
|
||||||
|
this.options.contactNames.length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<void> {
|
||||||
|
@ -137,10 +144,10 @@ export class Bootstrap {
|
||||||
|
|
||||||
const contactNames = this.options.contactNames.slice(
|
const contactNames = this.options.contactNames.slice(
|
||||||
0,
|
0,
|
||||||
this.options.contactCount
|
this.options.contactCount + this.options.contactsWithoutProfileKey
|
||||||
);
|
);
|
||||||
|
|
||||||
this.privContacts = await Promise.all(
|
const allContacts = await Promise.all(
|
||||||
contactNames.map(async profileName => {
|
contactNames.map(async profileName => {
|
||||||
const primary = await this.server.createPrimaryDevice({
|
const primary = await this.server.createPrimaryDevice({
|
||||||
profileName,
|
profileName,
|
||||||
|
@ -155,9 +162,15 @@ export class Bootstrap {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.privContacts = allContacts.slice(0, this.options.contactCount);
|
||||||
|
this.privContactsWithoutProfileKey = allContacts.slice(
|
||||||
|
this.contacts.length
|
||||||
|
);
|
||||||
|
|
||||||
this.privPhone = await this.server.createPrimaryDevice({
|
this.privPhone = await this.server.createPrimaryDevice({
|
||||||
profileName: 'Myself',
|
profileName: 'Myself',
|
||||||
contacts: this.contacts,
|
contacts: this.contacts,
|
||||||
|
contactsWithoutProfileKey: this.contactsWithoutProfileKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
|
this.storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'mock-signal-'));
|
||||||
|
@ -309,6 +322,14 @@ export class Bootstrap {
|
||||||
return this.privContacts;
|
return this.privContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get contactsWithoutProfileKey(): ReadonlyArray<PrimaryDevice> {
|
||||||
|
assert(
|
||||||
|
this.privContactsWithoutProfileKey,
|
||||||
|
'Bootstrap has to be initialized first, see: bootstrap.init()'
|
||||||
|
);
|
||||||
|
return this.privContactsWithoutProfileKey;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Private
|
// Private
|
||||||
//
|
//
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
import type { ElectronApplication, Page } from 'playwright';
|
import type { ElectronApplication, Page } from 'playwright';
|
||||||
import { _electron as electron } from 'playwright';
|
import { _electron as electron } from 'playwright';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IPCRequest as ChallengeRequestType,
|
||||||
|
IPCResponse as ChallengeResponseType,
|
||||||
|
} from '../challenge';
|
||||||
|
|
||||||
export type AppLoadedInfoType = Readonly<{
|
export type AppLoadedInfoType = Readonly<{
|
||||||
loadTime: number;
|
loadTime: number;
|
||||||
messagesPerSec: number;
|
messagesPerSec: number;
|
||||||
|
@ -57,6 +62,18 @@ export class App {
|
||||||
return this.waitForEvent('conversation:open');
|
return this.waitForEvent('conversation:open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async waitForChallenge(): Promise<ChallengeRequestType> {
|
||||||
|
return this.waitForEvent('challenge');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async solveChallenge(response: ChallengeResponseType): Promise<void> {
|
||||||
|
const window = await this.getWindow();
|
||||||
|
|
||||||
|
await window.evaluate(
|
||||||
|
`window.CI.solveChallenge(${JSON.stringify(response)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
await this.app.close();
|
await this.app.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
import { Proto, StorageState } from '@signalapp/mock-server';
|
||||||
|
|
||||||
|
import * as durations from '../../util/durations';
|
||||||
|
import { uuidToBytes } from '../../util/uuidToBytes';
|
||||||
|
import { MY_STORIES_ID } from '../../types/Stories';
|
||||||
|
import { Bootstrap } from '../bootstrap';
|
||||||
|
import type { App } from '../bootstrap';
|
||||||
|
|
||||||
|
export const debug = createDebug('mock:test:rate-limit');
|
||||||
|
|
||||||
|
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||||
|
|
||||||
|
describe('rate-limit/story', function needsName() {
|
||||||
|
this.timeout(durations.MINUTE);
|
||||||
|
|
||||||
|
let bootstrap: Bootstrap;
|
||||||
|
let app: App;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
bootstrap = new Bootstrap({
|
||||||
|
contactCount: 0,
|
||||||
|
contactsWithoutProfileKey: 40,
|
||||||
|
});
|
||||||
|
await bootstrap.init();
|
||||||
|
|
||||||
|
const { phone } = bootstrap;
|
||||||
|
|
||||||
|
let state = StorageState.getEmpty();
|
||||||
|
|
||||||
|
state = state.updateAccount({
|
||||||
|
profileKey: phone.profileKey.serialize(),
|
||||||
|
e164: phone.device.number,
|
||||||
|
givenName: phone.profileName,
|
||||||
|
hasSetMyStoriesPrivacy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
state = state.addRecord({
|
||||||
|
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
||||||
|
record: {
|
||||||
|
storyDistributionList: {
|
||||||
|
allowsReplies: true,
|
||||||
|
identifier: uuidToBytes(MY_STORIES_ID),
|
||||||
|
isBlockList: true,
|
||||||
|
name: MY_STORIES_ID,
|
||||||
|
recipientUuids: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
phone.setStorageState(state);
|
||||||
|
|
||||||
|
app = await bootstrap.link();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function after() {
|
||||||
|
if (this.currentTest?.state !== 'passed') {
|
||||||
|
await bootstrap.saveLogs(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
await bootstrap.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should request challenge and accept solution', async () => {
|
||||||
|
const {
|
||||||
|
server,
|
||||||
|
contactsWithoutProfileKey: contacts,
|
||||||
|
phone,
|
||||||
|
desktop,
|
||||||
|
} = bootstrap;
|
||||||
|
|
||||||
|
for (const contact of contacts) {
|
||||||
|
server.rateLimit({ source: desktop.uuid, target: contact.device.uuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
const window = await app.getWindow();
|
||||||
|
|
||||||
|
debug('Posting a new story');
|
||||||
|
{
|
||||||
|
const storiesPane = window.locator('.Stories');
|
||||||
|
|
||||||
|
await window.locator('button.module-main-header__stories-icon').click();
|
||||||
|
|
||||||
|
await storiesPane
|
||||||
|
.locator('button.Stories__pane__add-story__button')
|
||||||
|
.click();
|
||||||
|
await storiesPane
|
||||||
|
.locator(
|
||||||
|
'.ContextMenu__popper .Stories__pane__add-story__option--title ' +
|
||||||
|
'>> "Text story"'
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Focusing textarea');
|
||||||
|
await storiesPane.locator('.TextAttachment__story').click();
|
||||||
|
|
||||||
|
debug('Entering text');
|
||||||
|
await storiesPane.locator('.TextAttachment__text__textarea').type('123');
|
||||||
|
|
||||||
|
debug('Clicking "Next"');
|
||||||
|
await storiesPane
|
||||||
|
.locator('.StoryCreator__toolbar button >> "Next"')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Selecting "My Stories"');
|
||||||
|
await window
|
||||||
|
.locator('.SendStoryModal__distribution-list__name >> "My Stories"')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('Hitting Send');
|
||||||
|
await window.locator('button.SendStoryModal__send').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Waiting for challenge');
|
||||||
|
const request = await app.waitForChallenge();
|
||||||
|
|
||||||
|
debug('Checking for presence of captcha modal');
|
||||||
|
await window
|
||||||
|
.locator('.module-Modal__title >> "Verify to continue messaging"')
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
debug('Removing rate-limiting');
|
||||||
|
for (const contact of contacts) {
|
||||||
|
const failedMessages = server.stopRateLimiting({
|
||||||
|
source: desktop.uuid,
|
||||||
|
target: contact.device.uuid,
|
||||||
|
});
|
||||||
|
assert.isAtMost(failedMessages ?? 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Solving challenge');
|
||||||
|
app.solveChallenge({
|
||||||
|
seq: request.seq,
|
||||||
|
data: { captcha: 'anything' },
|
||||||
|
});
|
||||||
|
|
||||||
|
debug('Verifying that all contacts received story');
|
||||||
|
await Promise.all(
|
||||||
|
contacts.map(async contact => {
|
||||||
|
const { storyMessage } = await contact.waitForStory();
|
||||||
|
assert.isTrue(
|
||||||
|
phone.profileKey
|
||||||
|
.serialize()
|
||||||
|
.equals(storyMessage.profileKey ?? new Uint8Array(0))
|
||||||
|
);
|
||||||
|
assert.strictEqual(storyMessage.textAttachment?.text, '123');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1893,10 +1893,10 @@
|
||||||
node-gyp-build "^4.2.3"
|
node-gyp-build "^4.2.3"
|
||||||
uuid "^8.3.0"
|
uuid "^8.3.0"
|
||||||
|
|
||||||
"@signalapp/mock-server@2.8.1":
|
"@signalapp/mock-server@2.9.0":
|
||||||
version "2.8.1"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.8.1.tgz#f09c35de6fd084b0a654e2d20fd77095f7e5fca0"
|
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-2.9.0.tgz#7b86b7843cd828db5167d9152214646cabe76724"
|
||||||
integrity sha512-kQkzgtnVpFO9J00MpiTHBtKsyQhe/8PHy8nxJmIR3L2IQvZ8glbX4GCVh7dRIASY3IIJ8PpiAoYgePkL0DH8Gw==
|
integrity sha512-rowEcBwGb53C7gQTR9WTQrS3kO4WB43qTVlA2qeEUIzMruJb44klgrmo1QUiMTtGYuByDApbAC4yOcVNYmae4w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@signalapp/libsignal-client" "^0.20.0"
|
"@signalapp/libsignal-client" "^0.20.0"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
Loading…
Reference in New Issue