Signal-Desktop/ts/test-mock/messaging/backfill_test.ts

480 lines
14 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { PrimaryDevice } from '@signalapp/mock-server';
import { Proto } from '@signalapp/mock-server';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import createDebug from 'debug';
import type { Page } from 'playwright';
import assert from 'assert';
import Long from 'long';
import { LONG_MESSAGE, IMAGE_JPEG } from '../../types/MIME';
import * as durations from '../../util/durations';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
import { sendTextMessage, getTimelineMessageWithText } from '../helpers';
export const debug = createDebug('mock:test:edit');
const FIXTURES_PATH = join(__dirname, '..', '..', '..', 'fixtures');
const CAT_PATH = join(FIXTURES_PATH, 'cat-screenshot.png');
const SNOW_PATH = join(FIXTURES_PATH, 'snow.jpg');
const { Status } = Proto.SyncMessage.AttachmentBackfillResponse.AttachmentData;
function createResponse(
response: Proto.SyncMessage.IAttachmentBackfillResponse
): Proto.IContent {
return { syncMessage: { attachmentBackfillResponse: response } };
}
describe('attachment backfill', function (this: Mocha.Suite) {
this.timeout(durations.MINUTE);
let bootstrap: Bootstrap;
let app: App;
let page: Page;
let unknownContact: PrimaryDevice;
let textAttachment: Proto.IAttachmentPointer;
let catAttachment: Proto.IAttachmentPointer;
let snowAttachment: Proto.IAttachmentPointer;
beforeEach(async () => {
bootstrap = new Bootstrap({ contactCount: 1, unknownContactCount: 1 });
await bootstrap.init();
app = await bootstrap.link();
page = await app.getWindow();
const { unknownContacts } = bootstrap;
[unknownContact] = unknownContacts;
textAttachment = await bootstrap.storeAttachmentOnCDN(
Buffer.from('look at this pic, it is gorgeous!'),
LONG_MESSAGE
);
const plaintextCat = await readFile(CAT_PATH);
catAttachment = await bootstrap.storeAttachmentOnCDN(
plaintextCat,
IMAGE_JPEG
);
const plaintextSnow = await readFile(SNOW_PATH);
snowAttachment = await bootstrap.storeAttachmentOnCDN(
plaintextSnow,
IMAGE_JPEG
);
});
afterEach(async function (this: Mocha.Context) {
if (!bootstrap) {
return;
}
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app.close();
await bootstrap.teardown();
});
it('should be requested on manual download', async () => {
const { phone, desktop } = bootstrap;
debug('sending a message with attachment that is 404 on CDN');
const timestamp = bootstrap.getTimestamp();
await sendTextMessage({
from: unknownContact,
to: desktop,
desktop,
text: 'look at this pic!',
attachments: [
{ ...textAttachment, cdnKey: 'text-not-found' },
{ ...catAttachment, cdnKey: 'cat-not-found' },
{ ...snowAttachment, cdnKey: 'snow-not-found' },
],
timestamp,
});
debug('opening conversation');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
debug('dowloading attachment');
const conversationStack = page.locator('.Inbox__conversation-stack');
const startDownload = conversationStack.getByRole('button', {
name: 'Start Download',
});
await startDownload.click();
debug('waiting for backfill request');
const {
syncMessage: { attachmentBackfillRequest: request },
} = await phone.waitForSyncMessage(entry => {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
timestamp
);
// No download buttons
debug('waiting for spinner to become visible');
await startDownload.waitFor({ state: 'detached' });
const cancelDownload = conversationStack.getByRole('button', {
name: 'Cancel Download',
});
await cancelDownload.waitFor();
debug('sending pending backfill response');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
longText: { status: Status.PENDING },
attachments: [{ status: Status.PENDING }, { status: Status.PENDING }],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
debug('resolving long text');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
longText: { attachment: textAttachment },
attachments: [{ status: Status.PENDING }, { status: Status.PENDING }],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
await getTimelineMessageWithText(page, 'gorgeous').waitFor();
debug('resolving first attachment');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
longText: { attachment: textAttachment },
attachments: [
{ attachment: catAttachment },
{ status: Status.PENDING },
],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
await conversationStack
.getByRole('button', {
name: 'Open this attachment in a larger view',
})
.waitFor();
debug('failing second attachment');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
longText: { attachment: textAttachment },
attachments: [
{ attachment: catAttachment },
{ status: Status.TERMINAL_ERROR },
],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
await page.locator('.Toast >> "Download failed"').waitFor();
await cancelDownload.waitFor({ state: 'detached' });
await conversationStack
.getByRole('button', {
name: 'This media is not available',
})
.waitFor();
await conversationStack
.locator('.module-image__undownloadable-icon')
.waitFor();
});
it('should show modal on timeout', async () => {
const { desktop } = bootstrap;
debug('sending a message with attachment that is 404 on CDN');
const timestamp = bootstrap.getTimestamp();
await sendTextMessage({
from: unknownContact,
to: desktop,
desktop,
text: undefined,
attachments: [{ ...catAttachment, cdnKey: 'cat-not-found' }],
timestamp,
});
debug('opening conversation');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
debug('dowloading attachment');
const conversationStack = page.locator('.Inbox__conversation-stack');
const startDownload = conversationStack.getByRole('button', {
name: 'Start Download',
});
await startDownload.click();
debug('waiting for modal');
const modal = page.getByTestId('BackfillFailureModal');
await modal.waitFor();
await modal.locator('text=/internet connection/').waitFor();
});
it('should show modal on missing message', async () => {
const { phone, desktop } = bootstrap;
debug('sending a message with attachment that is 404 on CDN');
const timestamp = bootstrap.getTimestamp();
await sendTextMessage({
from: unknownContact,
to: desktop,
desktop,
text: undefined,
attachments: [{ ...catAttachment, cdnKey: 'cat-not-found' }],
timestamp,
});
debug('opening conversation');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
debug('dowloading attachment');
const conversationStack = page.locator('.Inbox__conversation-stack');
const startDownload = conversationStack.getByRole('button', {
name: 'Start Download',
});
await startDownload.click();
debug('waiting for request');
const {
syncMessage: { attachmentBackfillRequest: request },
} = await phone.waitForSyncMessage(entry => {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
timestamp
);
debug('sending not found response');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
error:
Proto.SyncMessage.AttachmentBackfillResponse.Error.MESSAGE_NOT_FOUND,
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
debug('waiting for modal');
const modal = page.getByTestId('BackfillFailureModal');
await modal.waitFor();
await modal.locator('text=/no longer available/').waitFor();
});
it('should resolve sticker', async () => {
const { phone, desktop } = bootstrap;
debug('sending a message with attachment that is 404 on CDN');
const timestamp = bootstrap.getTimestamp();
await sendTextMessage({
from: unknownContact,
to: desktop,
desktop,
text: undefined,
sticker: {
packId: Buffer.from('ae8fedafda4768fd3384d4b3b9db963d', 'hex'),
packKey: Buffer.from(
'53f4aa8b95e1c2e75afab2328fe67eb6d7affbcd4f50cd4da89dfc325dbc73ca',
'hex'
),
stickerId: 1,
emoji: '🐈',
data: { ...catAttachment, cdnKey: 'cat-not-found' },
},
timestamp,
});
debug('opening conversation');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
debug('dowloading attachment');
const conversationStack = page.locator('.Inbox__conversation-stack');
const startDownload = conversationStack.getByRole('button', {
name: 'Start Download',
});
await startDownload.click();
debug('waiting for backfill request');
const {
syncMessage: { attachmentBackfillRequest: request },
} = await phone.waitForSyncMessage(entry => {
return entry.syncMessage.attachmentBackfillRequest != null;
});
assert.deepEqual(
request?.targetConversation?.threadServiceIdBinary,
unknownContact.device.aciBinary
);
assert.deepEqual(
request?.targetMessage?.authorServiceIdBinary,
unknownContact.device.aciBinary
);
assert.strictEqual(
request?.targetMessage?.sentTimestamp?.toNumber(),
timestamp
);
debug('sending pending backfill response');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
attachments: [{ status: Status.PENDING }],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
debug('resolving sticker');
await phone.sendRaw(
desktop,
createResponse({
targetConversation: request.targetConversation,
targetMessage: request.targetMessage,
attachments: {
attachments: [{ attachment: catAttachment }],
},
}),
{
timestamp: bootstrap.getTimestamp(),
}
);
await conversationStack
.locator('.module-image-grid--with-sticker')
.waitFor();
});
it('should not request backfill on quote/preview', async () => {
const { desktop } = bootstrap;
debug('sending a message with attachment that is 404 on CDN');
const timestamp = bootstrap.getTimestamp();
await sendTextMessage({
from: unknownContact,
to: desktop,
desktop,
quote: {
id: Long.fromNumber(bootstrap.getTimestamp()),
authorAciBinary: unknownContact.device.aciRawUuid,
text: 'quote text',
attachments: [
{
contentType: IMAGE_JPEG,
fileName: 'snow.jpg',
thumbnail: { ...snowAttachment, cdnKey: 'snow-not-found' },
},
],
type: Proto.DataMessage.Quote.Type.NORMAL,
},
preview: {
url: 'https://signal.org',
title: 'Signal',
image: { ...catAttachment, cdnKey: 'cat-not-found' },
},
text: 'https://signal.org',
timestamp,
});
debug('opening conversation');
const leftPane = page.locator('#LeftPane');
const conversationListItem = leftPane.getByRole('button', {
name: 'Chat with Unknown contact',
});
await conversationListItem.getByText('Message Request').click();
debug('dowloading attachment');
const conversationStack = page.locator('.Inbox__conversation-stack');
const startDownload = conversationStack.getByRole('button', {
name: 'Start Download',
});
await startDownload.click();
await page.locator('.Toast >> "Download failed"').waitFor();
});
});