Files
Desktop/ts/test-mock/storage/delete_test.node.ts
T

502 lines
16 KiB
TypeScript

// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { Proto } from '@signalapp/mock-server';
import * as durations from '../../util/durations/index.std.ts';
import { initStorage } from './fixtures.node.ts';
import { debug } from './fixtures.node.ts';
import { constantTimeEqual, getRandomBytes } from '../../Crypto.node.ts';
import type { Bootstrap } from './fixtures.node.ts';
import type { App } from './fixtures.node.ts';
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
describe('storage service/delete', function (this: Mocha.Suite) {
this.timeout(durations.MINUTE);
let bootstrap: Bootstrap;
let app: App;
beforeEach(async () => {
({ bootstrap, app } = await initStorage());
});
afterEach(async function (this: Mocha.Context) {
if (!bootstrap) {
return;
}
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app.close();
await bootstrap.teardown();
});
it('should roundtrip records even if they were deleted when first discovered', async () => {
const { phone, contacts } = bootstrap;
const alice = contacts[0];
assert.exists(alice);
let state = await phone.expectStorageState('initial state');
debug('adding deleted records to storage service via phone');
const deletedAtTimestamp = BigInt(Date.now() + durations.DAY);
const storyDistributionList = {
type: IdentifierType.STORY_DISTRIBUTION_LIST,
key: Buffer.from(getRandomBytes(16)),
record: {
// if deletedAtTimestamp is set, name and members should not be
storyDistributionList: {
identifier: getRandomBytes(16),
name: null,
deletedAtTimestamp,
allowsReplies: null,
isBlockList: null,
recipientServiceIdsBinary: null,
},
},
};
state = state.addRecord(storyDistributionList);
const stickerPack = {
type: IdentifierType.STICKER_PACK,
key: Buffer.from(getRandomBytes(16)),
record: {
stickerPack: {
packId: getRandomBytes(16),
packKey: getRandomBytes(32),
position: 1,
deletedAtTimestamp,
},
},
};
state = state.addRecord(stickerPack);
const callLink = {
type: IdentifierType.CALL_LINK,
key: Buffer.from(getRandomBytes(16)),
record: {
// if deletedAtTimestampMs is set, adminPassKey should not be
callLink: {
rootKey: getRandomBytes(16),
adminPasskey: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
state = state.addRecord(callLink);
const chatFolder = {
type: IdentifierType.CHAT_FOLDER,
key: Buffer.from(getRandomBytes(16)),
record: {
chatFolder: {
id: getRandomBytes(16),
name: 'Chat Folder',
position: 4294967295,
showOnlyUnread: null,
showMutedChats: null,
includeAllIndividualChats: null,
includeAllGroupChats: null,
folderType: Proto.ChatFolderRecord.FolderType.CUSTOM,
includedRecipients: null,
excludedRecipients: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
state = state.addRecord(chatFolder);
const notificationProfile = {
type: IdentifierType.NOTIFICATION_PROFILE,
key: Buffer.from(getRandomBytes(16)),
record: {
notificationProfile: {
id: getRandomBytes(16),
name: 'Notification Profile',
emoji: null,
color: null,
createdAtMs: null,
allowAllCalls: null,
allowAllMentions: null,
allowedMembers: null,
scheduleEnabled: null,
scheduleStartTime: null,
scheduleEndTime: null,
scheduleDaysEnabled: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
state = state.addRecord(notificationProfile);
state = state.pin(alice);
const updatedState = await phone.setStorageState(state);
await phone.sendFetchStorage({
timestamp: bootstrap.getTimestamp(),
});
debug('waiting for Desktop to pick up the change');
await app.waitForManifestVersion(updatedState.version);
const window = await app.getWindow();
const conversationStack = window.locator('.Inbox__conversation-stack');
const leftPane = window.locator('#LeftPane');
debug('verifying that contact is pinned');
await leftPane.locator(`[data-testid="${alice.device.aci}"]`).waitFor();
debug('unpinning via desktop');
{
const convo = leftPane.getByTestId(alice.device.aci);
await convo.click();
const moreButton = conversationStack.getByRole('button', {
name: 'More Info',
});
await moreButton.click();
const pinButton = window.getByRole('menuitem', {
name: 'Unpin chat',
exact: true,
});
await pinButton.click();
}
debug("waiting for desktop's storage service update to get back to phone");
const newState = await phone.waitForStorageState({
after: updatedState,
predicate: maybeState => !maybeState.isPinned(alice),
});
debug(
"validating what's in storage service - deleted items should still be there"
);
assert.isTrue(
newState.hasRecord(
item =>
constantTimeEqual(item.key, storyDistributionList.key) &&
item.record.storyDistributionList?.deletedAtTimestamp ===
storyDistributionList.record.storyDistributionList
?.deletedAtTimestamp
),
'looking for deleted storyDistribution list'
);
assert.isTrue(
newState.hasRecord(
item =>
constantTimeEqual(item.key, stickerPack.key) &&
item.record.stickerPack?.deletedAtTimestamp ===
stickerPack.record.stickerPack?.deletedAtTimestamp
),
'looking for deleted stickerPack list'
);
assert.isTrue(
newState.hasRecord(
item =>
constantTimeEqual(item.key, callLink.key) &&
item.record.callLink?.deletedAtTimestampMs ===
callLink.record.callLink?.deletedAtTimestampMs
),
'looking for deleted callLink list'
);
assert.isTrue(
newState.hasRecord(
item =>
constantTimeEqual(item.key, chatFolder.key) &&
item.record.chatFolder?.deletedAtTimestampMs ===
chatFolder.record.chatFolder?.deletedAtTimestampMs
),
'looking for deleted chatFolder list'
);
assert.isTrue(
newState.hasRecord(
item =>
constantTimeEqual(item.key, notificationProfile.key) &&
item.record.notificationProfile?.deletedAtTimestampMs ===
notificationProfile.record.notificationProfile?.deletedAtTimestampMs
),
'looking for deleted notificationProfile list'
);
});
it('should not restore records if they were deleted and removed from storage service', async () => {
const { phone, contacts } = bootstrap;
const alice = contacts[0];
assert.exists(alice);
let initialState = await phone.expectStorageState('initial state');
debug('adding deleted records to storage service via phone');
const deletedAtTimestamp = BigInt(Date.now() + durations.DAY);
const storyDistributionList = {
type: IdentifierType.STORY_DISTRIBUTION_LIST,
key: Buffer.from(getRandomBytes(16)),
record: {
// if deletedAtTimestamp is set, name and members should not be
storyDistributionList: {
identifier: getRandomBytes(16),
name: null,
deletedAtTimestamp,
allowsReplies: null,
isBlockList: null,
recipientServiceIdsBinary: null,
},
},
};
initialState = initialState.addRecord(storyDistributionList);
const stickerPack = {
type: IdentifierType.STICKER_PACK,
key: Buffer.from(getRandomBytes(16)),
record: {
stickerPack: {
packId: getRandomBytes(16),
packKey: getRandomBytes(32),
position: 1,
deletedAtTimestamp,
},
},
};
initialState = initialState.addRecord(stickerPack);
const callLink = {
type: IdentifierType.CALL_LINK,
key: Buffer.from(getRandomBytes(16)),
record: {
// if deletedAtTimestampMs is set, adminPassKey should not be
callLink: {
rootKey: getRandomBytes(16),
adminPasskey: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
initialState = initialState.addRecord(callLink);
const chatFolder = {
type: IdentifierType.CHAT_FOLDER,
key: Buffer.from(getRandomBytes(16)),
record: {
chatFolder: {
id: getRandomBytes(16),
name: 'Chat Folder',
position: 4294967295,
showOnlyUnread: null,
showMutedChats: null,
includeAllIndividualChats: null,
includeAllGroupChats: null,
folderType: Proto.ChatFolderRecord.FolderType.CUSTOM,
includedRecipients: null,
excludedRecipients: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
initialState = initialState.addRecord(chatFolder);
const notificationProfile = {
type: IdentifierType.NOTIFICATION_PROFILE,
key: Buffer.from(getRandomBytes(16)),
record: {
notificationProfile: {
id: getRandomBytes(16),
name: 'Notification Profile',
emoji: null,
color: null,
createdAtMs: null,
allowAllCalls: null,
allowAllMentions: null,
allowedMembers: null,
scheduleEnabled: null,
scheduleStartTime: null,
scheduleEndTime: null,
scheduleDaysEnabled: null,
deletedAtTimestampMs: deletedAtTimestamp,
},
},
};
initialState = initialState.addRecord(notificationProfile);
initialState = initialState.pin(alice);
const firstPhoneState = await phone.setStorageState(initialState);
await phone.sendFetchStorage({
timestamp: bootstrap.getTimestamp(),
});
debug('waiting for Desktop to pick up the first change');
await app.waitForManifestVersion(firstPhoneState.version);
const window = await app.getWindow();
const conversationStack = window.locator('.Inbox__conversation-stack');
const leftPane = window.locator('#LeftPane');
debug('verifying that contact is pinned');
await leftPane.locator(`[data-testid="${alice.device.aci}"]`).waitFor();
debug('unpinning via desktop');
{
const convo = leftPane.getByTestId(alice.device.aci);
await convo.click();
const moreButton = conversationStack.getByRole('button', {
name: 'More Info',
});
await moreButton.click();
const pinButton = window.getByRole('menuitem', {
name: 'Unpin chat',
exact: true,
});
await pinButton.click();
}
debug("waiting for desktop's storage service update to get back to phone");
let updateState = await phone.waitForStorageState({
after: firstPhoneState,
predicate: maybeState => !maybeState.isPinned(alice),
});
debug('now removing items from storage service via phone');
updateState = updateState.removeRecord(
item =>
constantTimeEqual(item.key, storyDistributionList.key) &&
item.record.storyDistributionList?.deletedAtTimestamp ===
storyDistributionList.record.storyDistributionList?.deletedAtTimestamp
);
updateState = updateState.removeRecord(
item =>
constantTimeEqual(item.key, stickerPack.key) &&
item.record.stickerPack?.deletedAtTimestamp ===
stickerPack.record.stickerPack?.deletedAtTimestamp
);
updateState = updateState.removeRecord(
item =>
constantTimeEqual(item.key, callLink.key) &&
item.record.callLink?.deletedAtTimestampMs ===
callLink.record.callLink?.deletedAtTimestampMs
);
updateState = updateState.removeRecord(
item =>
constantTimeEqual(item.key, chatFolder.key) &&
item.record.chatFolder?.deletedAtTimestampMs ===
chatFolder.record.chatFolder?.deletedAtTimestampMs
);
updateState = updateState.removeRecord(
item =>
constantTimeEqual(item.key, notificationProfile.key) &&
item.record.notificationProfile?.deletedAtTimestampMs ===
notificationProfile.record.notificationProfile?.deletedAtTimestampMs
);
updateState = updateState.pin(alice);
const secondPhoneState = await phone.setStorageState(updateState);
await phone.sendFetchStorage({
timestamp: bootstrap.getTimestamp(),
});
debug('waiting for Desktop to pick up the second change');
await app.waitForManifestVersion(secondPhoneState.version);
debug('verifying that contact is pinned');
await leftPane.locator(`[data-testid="${alice.device.aci}"]`).waitFor();
debug('unpinning via desktop');
{
const convo = leftPane.getByTestId(alice.device.aci);
await convo.click();
const moreButton = conversationStack.getByRole('button', {
name: 'More Info',
});
await moreButton.click();
const pinButton = window.getByRole('menuitem', {
name: 'Unpin chat',
exact: true,
});
await pinButton.click();
}
debug("waiting for desktop's storage service update to get back to phone");
const thirdPhoneState = await phone.waitForStorageState({
after: secondPhoneState,
predicate: maybeState => !maybeState.isPinned(alice),
});
debug(
"validating what's in storage service - deleted items should be removed"
);
assert.isFalse(
thirdPhoneState.hasRecord(item => {
const itemId = item.record.storyDistributionList?.identifier;
const expectedId =
storyDistributionList.record.storyDistributionList?.identifier;
if (!itemId || !expectedId) {
return false;
}
return constantTimeEqual(itemId, expectedId);
}),
"don't want to find deleted storyDistribution list"
);
assert.isFalse(
thirdPhoneState.hasRecord(item => {
const itemId = item.record.stickerPack?.packId;
const expectedId = stickerPack.record.stickerPack?.packId;
if (!itemId || !expectedId) {
return false;
}
return constantTimeEqual(itemId, expectedId);
}),
"don't want to find deleted stickerPack list"
);
assert.isFalse(
thirdPhoneState.hasRecord(item => {
const itemId = item.record.callLink?.rootKey;
const expectedId = callLink.record.callLink?.rootKey;
if (!itemId || !expectedId) {
return false;
}
return constantTimeEqual(itemId, expectedId);
}),
"don't want to find deleted callLink list"
);
assert.isFalse(
thirdPhoneState.hasRecord(item => {
const itemId = item.record.chatFolder?.id;
const expectedId = chatFolder.record.chatFolder?.id;
if (!itemId || !expectedId) {
return false;
}
return constantTimeEqual(itemId, expectedId);
}),
"don't want to find deleted chatFolder list"
);
assert.isFalse(
thirdPhoneState.hasRecord(item => {
const itemId = item.record.notificationProfile?.id;
const expectedId = notificationProfile.record.notificationProfile?.id;
if (!itemId || !expectedId) {
return false;
}
return constantTimeEqual(itemId, expectedId);
}),
"don't want to find deleted notificationProfile list"
);
});
});