2023-05-25 12:12:33 +00:00
|
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import createDebug from 'debug';
|
|
|
|
import Long from 'long';
|
|
|
|
import { Proto, StorageState } from '@signalapp/mock-server';
|
2023-05-31 20:07:43 +00:00
|
|
|
import type { Group } from '@signalapp/mock-server';
|
2023-05-25 12:12:33 +00:00
|
|
|
|
|
|
|
import * as durations from '../../util/durations';
|
|
|
|
import { uuidToBytes } from '../../util/uuidToBytes';
|
|
|
|
import { MY_STORY_ID } from '../../types/Stories';
|
2023-08-10 16:43:33 +00:00
|
|
|
import { generateStoryDistributionId } from '../../types/StoryDistributionId';
|
2023-05-25 12:12:33 +00:00
|
|
|
import type { App } from '../playwright';
|
|
|
|
import { Bootstrap } from '../bootstrap';
|
|
|
|
|
2023-10-04 00:12:57 +00:00
|
|
|
export const debug = createDebug('mock:test:stories');
|
2023-05-25 12:12:33 +00:00
|
|
|
|
|
|
|
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
|
|
|
|
2023-08-10 16:43:33 +00:00
|
|
|
const DISTRIBUTION1 = generateStoryDistributionId();
|
|
|
|
const DISTRIBUTION2 = generateStoryDistributionId();
|
2023-05-25 12:12:33 +00:00
|
|
|
|
2023-10-11 19:06:43 +00:00
|
|
|
describe('story/messaging', function (this: Mocha.Suite) {
|
2023-05-25 12:12:33 +00:00
|
|
|
this.timeout(durations.MINUTE);
|
|
|
|
|
|
|
|
let bootstrap: Bootstrap;
|
|
|
|
let app: App;
|
2023-05-31 20:07:43 +00:00
|
|
|
let group: Group;
|
2023-05-25 12:12:33 +00:00
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
bootstrap = new Bootstrap();
|
|
|
|
await bootstrap.init();
|
|
|
|
|
|
|
|
const { phone, contacts } = bootstrap;
|
|
|
|
const [first, second] = contacts;
|
|
|
|
|
|
|
|
let state = StorageState.getEmpty();
|
|
|
|
|
|
|
|
state = state.updateAccount({
|
|
|
|
profileKey: phone.profileKey.serialize(),
|
|
|
|
e164: phone.device.number,
|
|
|
|
givenName: phone.profileName,
|
|
|
|
hasSetMyStoriesPrivacy: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create empty My Story
|
|
|
|
state = state.addRecord({
|
|
|
|
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
|
|
|
record: {
|
|
|
|
storyDistributionList: {
|
|
|
|
allowsReplies: true,
|
|
|
|
identifier: uuidToBytes(MY_STORY_ID),
|
|
|
|
isBlockList: false,
|
|
|
|
name: MY_STORY_ID,
|
2023-08-16 20:54:39 +00:00
|
|
|
recipientServiceIds: [],
|
2023-05-25 12:12:33 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create two distribution lists corresponding to two contacts
|
|
|
|
state = state.addRecord({
|
|
|
|
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
|
|
|
record: {
|
|
|
|
storyDistributionList: {
|
|
|
|
allowsReplies: true,
|
|
|
|
identifier: uuidToBytes(DISTRIBUTION1),
|
|
|
|
isBlockList: false,
|
|
|
|
name: 'first',
|
2023-08-16 20:54:39 +00:00
|
|
|
recipientServiceIds: [first.device.aci],
|
2023-05-25 12:12:33 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
state = state.addRecord({
|
|
|
|
type: IdentifierType.STORY_DISTRIBUTION_LIST,
|
|
|
|
record: {
|
|
|
|
storyDistributionList: {
|
|
|
|
allowsReplies: true,
|
|
|
|
identifier: uuidToBytes(DISTRIBUTION2),
|
|
|
|
isBlockList: false,
|
|
|
|
name: 'second',
|
2023-08-16 20:54:39 +00:00
|
|
|
recipientServiceIds: [second.device.aci],
|
2023-05-25 12:12:33 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-05-31 20:07:43 +00:00
|
|
|
// Add a group for story send
|
|
|
|
const members = [...contacts].slice(0, 10);
|
|
|
|
group = await phone.createGroup({
|
|
|
|
title: 'Mock Group',
|
|
|
|
members: [phone, ...members],
|
|
|
|
});
|
|
|
|
|
|
|
|
state = state
|
|
|
|
.addGroup(group, {
|
|
|
|
whitelisted: true,
|
|
|
|
storySendMode: Proto.GroupV2Record.StorySendMode.ENABLED,
|
|
|
|
})
|
|
|
|
.pinGroup(group);
|
|
|
|
|
2023-05-25 12:12:33 +00:00
|
|
|
// Finally whitelist and pin contacts
|
|
|
|
for (const contact of [first, second]) {
|
|
|
|
state = state.addContact(contact, {
|
|
|
|
whitelisted: true,
|
|
|
|
serviceE164: contact.device.number,
|
|
|
|
identityKey: contact.publicKey.serialize(),
|
|
|
|
profileKey: contact.profileKey.serialize(),
|
|
|
|
givenName: contact.profileName,
|
|
|
|
});
|
|
|
|
state = state.pin(contact);
|
|
|
|
}
|
|
|
|
|
|
|
|
await phone.setStorageState(state);
|
|
|
|
app = await bootstrap.link();
|
|
|
|
});
|
|
|
|
|
2023-10-11 19:06:43 +00:00
|
|
|
afterEach(async function (this: Mocha.Context) {
|
2023-05-25 12:12:33 +00:00
|
|
|
if (!bootstrap) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-14 16:53:20 +00:00
|
|
|
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
2023-05-25 12:12:33 +00:00
|
|
|
await app.close();
|
|
|
|
await bootstrap.teardown();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allows replies on multiple distribution lists', async () => {
|
|
|
|
const { phone, desktop, contacts } = bootstrap;
|
|
|
|
const [first, second] = contacts;
|
|
|
|
|
|
|
|
const window = await app.getWindow();
|
|
|
|
const sentAt = Date.now();
|
|
|
|
|
|
|
|
debug('waiting for storage service sync to complete');
|
|
|
|
await app.waitForStorageService();
|
|
|
|
|
|
|
|
debug('sending story sync message');
|
|
|
|
await phone.sendRaw(
|
|
|
|
desktop,
|
|
|
|
{
|
|
|
|
syncMessage: {
|
|
|
|
sent: {
|
|
|
|
timestamp: Long.fromNumber(sentAt),
|
|
|
|
expirationStartTimestamp: Long.fromNumber(sentAt),
|
|
|
|
storyMessage: {
|
|
|
|
textAttachment: {
|
|
|
|
text: 'hello',
|
|
|
|
},
|
|
|
|
allowsReplies: true,
|
|
|
|
},
|
|
|
|
storyMessageRecipients: [
|
|
|
|
{
|
2023-08-16 20:54:39 +00:00
|
|
|
destinationServiceId: first.device.aci,
|
2023-05-25 12:12:33 +00:00
|
|
|
distributionListIds: [DISTRIBUTION1],
|
|
|
|
isAllowedToReply: true,
|
|
|
|
},
|
|
|
|
{
|
2023-08-16 20:54:39 +00:00
|
|
|
destinationServiceId: second.device.aci,
|
2023-05-25 12:12:33 +00:00
|
|
|
distributionListIds: [DISTRIBUTION2],
|
|
|
|
isAllowedToReply: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ timestamp: sentAt }
|
|
|
|
);
|
|
|
|
|
|
|
|
debug('sending story replies');
|
|
|
|
await first.sendRaw(
|
|
|
|
desktop,
|
|
|
|
{
|
|
|
|
dataMessage: {
|
|
|
|
body: 'first reply',
|
|
|
|
storyContext: {
|
2023-08-16 20:54:39 +00:00
|
|
|
authorAci: phone.device.aci,
|
2023-05-25 12:12:33 +00:00
|
|
|
sentTimestamp: Long.fromNumber(sentAt),
|
|
|
|
},
|
|
|
|
timestamp: Long.fromNumber(sentAt + 1),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ timestamp: sentAt + 1 }
|
|
|
|
);
|
|
|
|
await second.sendRaw(
|
|
|
|
desktop,
|
|
|
|
{
|
|
|
|
dataMessage: {
|
|
|
|
body: 'second reply',
|
|
|
|
storyContext: {
|
2023-08-16 20:54:39 +00:00
|
|
|
authorAci: phone.device.aci,
|
2023-05-25 12:12:33 +00:00
|
|
|
sentTimestamp: Long.fromNumber(sentAt),
|
|
|
|
},
|
|
|
|
timestamp: Long.fromNumber(sentAt + 2),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ timestamp: sentAt + 2 }
|
|
|
|
);
|
|
|
|
|
2023-07-26 22:23:32 +00:00
|
|
|
const leftPane = window.locator('#LeftPane');
|
2023-05-25 12:12:33 +00:00
|
|
|
|
|
|
|
debug('Finding both replies');
|
|
|
|
await leftPane
|
2023-08-16 20:54:39 +00:00
|
|
|
.locator(`[data-testid="${first.device.aci}"] >> "first reply"`)
|
2023-05-25 12:12:33 +00:00
|
|
|
.waitFor();
|
|
|
|
await leftPane
|
2023-08-16 20:54:39 +00:00
|
|
|
.locator(`[data-testid="${second.device.aci}"] >> "second reply"`)
|
2023-05-25 12:12:33 +00:00
|
|
|
.waitFor();
|
|
|
|
});
|
2023-05-31 20:07:43 +00:00
|
|
|
|
|
|
|
it('allows replies to groups', async () => {
|
|
|
|
const { desktop, contacts } = bootstrap;
|
|
|
|
|
|
|
|
const window = await app.getWindow();
|
|
|
|
|
|
|
|
debug('waiting for storage service sync to complete');
|
|
|
|
await app.waitForStorageService();
|
|
|
|
|
2023-08-09 00:53:06 +00:00
|
|
|
await window.getByTestId('NavTabsItem--Stories').click();
|
2023-05-31 20:07:43 +00:00
|
|
|
|
|
|
|
debug('Create and send a story to the group');
|
|
|
|
await window.getByRole('button', { name: 'Add a story' }).first().click();
|
|
|
|
await window.getByRole('button', { name: 'Text story' }).click();
|
2023-11-27 23:44:53 +00:00
|
|
|
// Note: For some reason `.click()` doesn't work here anymore.
|
|
|
|
await window.locator('.TextAttachment').dispatchEvent('click');
|
|
|
|
await window.getByRole('textbox', { name: 'Add text' }).fill('hello');
|
2023-05-31 20:07:43 +00:00
|
|
|
await window.getByRole('button', { name: 'Next' }).click();
|
|
|
|
await window
|
|
|
|
.locator('.Checkbox__container')
|
|
|
|
.getByText('Mock Group')
|
|
|
|
.click();
|
|
|
|
await window.getByRole('button', { name: 'Send story' }).click();
|
|
|
|
|
|
|
|
// Grab the time the story was sent at
|
|
|
|
const time = await window.locator('time').nth(1).getAttribute('datetime');
|
|
|
|
if (!time) {
|
|
|
|
throw new Error('Cannot locate story time');
|
|
|
|
}
|
|
|
|
const sentAt = new Date(time).valueOf();
|
|
|
|
|
2023-10-04 00:12:57 +00:00
|
|
|
debug('Contact sends reply to group story', {
|
|
|
|
story: sentAt,
|
|
|
|
reply: sentAt + 1,
|
|
|
|
});
|
2023-05-31 20:07:43 +00:00
|
|
|
await contacts[0].sendRaw(
|
|
|
|
desktop,
|
|
|
|
{
|
|
|
|
dataMessage: {
|
|
|
|
body: 'first reply',
|
|
|
|
storyContext: {
|
2023-08-16 20:54:39 +00:00
|
|
|
authorAci: desktop.aci,
|
2023-05-31 20:07:43 +00:00
|
|
|
sentTimestamp: Long.fromNumber(sentAt),
|
|
|
|
},
|
|
|
|
groupV2: {
|
|
|
|
masterKey: group.masterKey,
|
|
|
|
},
|
|
|
|
timestamp: Long.fromNumber(sentAt + 1),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ timestamp: sentAt + 1 }
|
|
|
|
);
|
|
|
|
|
|
|
|
debug('Ensure sender sees the reply');
|
|
|
|
await window
|
|
|
|
.locator('.StoryListItem__button')
|
|
|
|
.getByText('Mock Group')
|
|
|
|
.click();
|
|
|
|
// For some reason we need to click the story & exit before the reply shows up
|
|
|
|
await window.getByRole('button', { name: 'Close' }).click();
|
|
|
|
await window
|
|
|
|
.locator('.StoryListItem__button')
|
|
|
|
.getByText('Mock Group')
|
|
|
|
.click();
|
|
|
|
|
|
|
|
await window.getByText('1 reply').click();
|
|
|
|
await window.getByText('first reply').waitFor();
|
|
|
|
});
|
2023-05-25 12:12:33 +00:00
|
|
|
});
|