Files
Desktop/ts/test-electron/deleteMessageAttachments_test.preload.ts
Fedor Indutny 44076ece79 Rename files
2025-10-16 23:45:44 -07:00

305 lines
8.4 KiB
TypeScript

// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import fsExtra from 'fs-extra';
import { v4 as generateUuid } from 'uuid';
import { readdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { missingCaseError } from '../util/missingCaseError.std.js';
import {
getDownloadsPath,
getPath,
} from '../windows/main/attachments.preload.js';
import { IMAGE_JPEG, LONG_MESSAGE } from '../types/MIME.std.js';
import type { MessageAttributesType } from '../model-types.d.ts';
import type { AttachmentType } from '../types/Attachment.std.js';
import { deleteAllAttachmentFilesOnDisk } from '../util/Attachment.std.js';
import {
getAbsoluteAttachmentPath,
getAbsoluteDownloadsPath,
getAbsoluteDraftPath,
deleteAttachmentData,
deleteDownloadData,
deleteExternalMessageFiles,
} from '../util/migrations.preload.js';
import { strictAssert } from '../util/assert.std.js';
const { emptyDir, ensureFile } = fsExtra;
function getAbsolutePath(
path: string,
type: 'attachment' | 'download' | 'draft'
) {
switch (type) {
case 'attachment':
return getAbsoluteAttachmentPath(path);
case 'download':
return getAbsoluteDownloadsPath(path);
case 'draft':
return getAbsoluteDraftPath(path);
default:
throw missingCaseError(type);
}
}
async function writeFile(
path: string,
type: 'attachment' | 'download' | 'draft'
) {
await ensureFile(getAbsolutePath(path, type));
}
async function writeFiles(
num: number,
type: 'attachment' | 'download' | 'draft'
) {
for (let i = 1; i <= num; i += 1) {
// eslint-disable-next-line no-await-in-loop
await writeFile(`${type}${i}`, type);
}
}
function listFiles(type: 'attachment' | 'download' | 'draft'): Array<string> {
return readdirSync(dirname(getAbsolutePath('fakename', type)));
}
let attachmentIndex = 0;
let downloadIndex = 0;
describe('Attachment deletion', () => {
beforeEach(async () => {
attachmentIndex = 0;
downloadIndex = 0;
await emptyDir(getPath(window.SignalContext.config.userDataPath));
await emptyDir(getDownloadsPath(window.SignalContext.config.userDataPath));
});
afterEach(async () => {
await emptyDir(getPath(window.SignalContext.config.userDataPath));
await emptyDir(getDownloadsPath(window.SignalContext.config.userDataPath));
});
function getAttachmentFilePath() {
attachmentIndex += 1;
return `attachment${attachmentIndex}`;
}
function getDownloadFilePath() {
downloadIndex += 1;
return `download${downloadIndex}`;
}
function composeAttachment(): AttachmentType {
return {
contentType: IMAGE_JPEG,
size: 128,
path: getAttachmentFilePath(),
downloadPath: getDownloadFilePath(),
thumbnail: {
contentType: IMAGE_JPEG,
size: 128,
path: getAttachmentFilePath(),
},
screenshot: {
contentType: IMAGE_JPEG,
size: 128,
path: getAttachmentFilePath(),
},
thumbnailFromBackup: {
contentType: IMAGE_JPEG,
size: 128,
path: getAttachmentFilePath(),
},
};
}
function composeBodyAttachment() {
return {
contentType: LONG_MESSAGE,
size: 128,
path: getAttachmentFilePath(),
downloadPath: getDownloadFilePath(),
};
}
it('deleteAllAttachmentFilesOnDisk deletes all paths referenced', async () => {
await writeFiles(5, 'attachment');
await writeFiles(3, 'download');
await deleteAllAttachmentFilesOnDisk({
deleteAttachmentOnDisk: deleteAttachmentData,
deleteDownloadOnDisk: deleteDownloadData,
})(composeAttachment());
assert.strictEqual(attachmentIndex, 4);
assert.sameDeepMembers(listFiles('attachment'), ['attachment5']);
assert.sameDeepMembers(listFiles('download'), ['download2', 'download3']);
});
it('deleteAllAttachmentFilesOnDisk does not delete files for copied attachments', async () => {
await writeFiles(5, 'attachment');
await writeFiles(5, 'download');
const attachment = composeAttachment();
attachment.copied = true;
await deleteAllAttachmentFilesOnDisk({
deleteAttachmentOnDisk: deleteAttachmentData,
deleteDownloadOnDisk: deleteDownloadData,
})(attachment);
assert.sameDeepMembers(listFiles('attachment'), [
'attachment1',
'attachment2',
'attachment3',
'attachment4',
'attachment5',
]);
assert.sameDeepMembers(listFiles('download'), [
'download1',
'download2',
'download3',
'download4',
'download5',
]);
});
// Update these if more paths are added to composeMessageWithAllAttachments
const NUM_ATTACHMENT_FILES_IN_MESSAGE = 42;
const NUM_DOWNLOAD_FILES_IN_MESSAGE = 12;
function composeMessageWithAllAttachments(): MessageAttributesType {
const message: MessageAttributesType = {
id: generateUuid(),
type: 'outgoing',
sent_at: Date.now(),
timestamp: Date.now(),
received_at: Date.now(),
conversationId: generateUuid(),
bodyAttachment: composeBodyAttachment(),
attachments: [composeAttachment(), composeAttachment()],
contact: [
{
avatar: {
isProfile: false,
avatar: composeAttachment(),
},
},
],
preview: [
{
url: 'url',
image: composeAttachment(),
},
],
quote: {
id: Date.now(),
isViewOnce: false,
referencedMessageNotFound: false,
attachments: [
{
contentType: IMAGE_JPEG,
thumbnail: composeAttachment(),
},
],
},
sticker: {
packId: 'packId',
stickerId: 42,
packKey: 'packKey',
data: composeAttachment(),
},
};
message.editHistory = [
{
timestamp: Date.now(),
received_at: Date.now(),
bodyAttachment: message.bodyAttachment,
attachments: message.attachments,
preview: message.preview,
quote: message.quote,
},
{
timestamp: Date.now(),
received_at: Date.now(),
bodyAttachment: composeBodyAttachment(),
attachments: [composeAttachment(), composeAttachment()],
preview: [
{
url: 'url',
image: composeAttachment(),
},
],
quote: {
id: Date.now(),
isViewOnce: false,
referencedMessageNotFound: false,
attachments: [
{
contentType: IMAGE_JPEG,
thumbnail: composeAttachment(),
},
],
},
},
];
return message;
}
it('deleteExternalMessageFiles deletes all message attachments, including editHistory', async () => {
await writeFiles(NUM_ATTACHMENT_FILES_IN_MESSAGE + 3, 'attachment');
await writeFiles(NUM_DOWNLOAD_FILES_IN_MESSAGE + 3, 'download');
const message = composeMessageWithAllAttachments();
await deleteExternalMessageFiles(message);
assert.strictEqual(attachmentIndex, NUM_ATTACHMENT_FILES_IN_MESSAGE);
assert.strictEqual(downloadIndex, NUM_DOWNLOAD_FILES_IN_MESSAGE);
assert.sameDeepMembers(listFiles('attachment'), [
'attachment43',
'attachment44',
'attachment45',
]);
assert.sameDeepMembers(listFiles('download'), [
'download13',
'download14',
'download15',
]);
});
it('deleteExternalMessageFiles does not delete copied quote attachments', async () => {
await writeFiles(NUM_ATTACHMENT_FILES_IN_MESSAGE + 3, 'attachment');
await writeFiles(NUM_DOWNLOAD_FILES_IN_MESSAGE + 3, 'download');
const message = composeMessageWithAllAttachments();
const quotedThumbnail = message.quote?.attachments[0].thumbnail;
strictAssert(quotedThumbnail, 'thumbnail exists');
quotedThumbnail.copied = true;
await deleteExternalMessageFiles(message);
assert.strictEqual(attachmentIndex, NUM_ATTACHMENT_FILES_IN_MESSAGE);
assert.strictEqual(downloadIndex, NUM_DOWNLOAD_FILES_IN_MESSAGE);
assert.sameDeepMembers(listFiles('attachment'), [
quotedThumbnail.path,
quotedThumbnail.thumbnail?.path,
quotedThumbnail.thumbnailFromBackup?.path,
quotedThumbnail.screenshot?.path,
'attachment43',
'attachment44',
'attachment45',
]);
assert.sameDeepMembers(listFiles('download'), [
quotedThumbnail.downloadPath,
'download13',
'download14',
'download15',
]);
});
});