signal-desktop/ts/test-electron/backup/backup_groupv2_notifications_test.ts

2298 lines
56 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import path from 'path';
import { tmpdir } from 'os';
import { rmSync, mkdtempSync, createReadStream } from 'fs';
import { v4 as generateGuid } from 'uuid';
import { assert } from 'chai';
import { pick, sortBy } from 'lodash';
import Data from '../../sql/Client';
import { backupsService } from '../../services/backups';
import { generateAci, generatePni } from '../../types/ServiceId';
import { SignalService as Proto } from '../../protobuf';
import type { MessageAttributesType } from '../../model-types';
import type { GroupV2ChangeType } from '../../groups';
import { getRandomBytes } from '../../Crypto';
import * as Bytes from '../../Bytes';
import { loadCallsHistory } from '../../services/callHistoryLoader';
import { strictAssert } from '../../util/assert';
import { DurationInSeconds } from '../../util/durations';
// Note: this should be kept up to date with GroupV2Change.stories.tsx, to
// maintain the comprehensive set of GroupV2 notifications we need to handle
const AccessControlEnum = Proto.AccessControl.AccessRequired;
const RoleEnum = Proto.Member.Role;
const EXPIRATION_TIMER_FLAG = Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const OUR_ACI = generateAci();
const OUR_PNI = generatePni();
const CONTACT_A = generateAci();
const CONTACT_A_PNI = generatePni();
const CONTACT_B = generateAci();
const CONTACT_C = generateAci();
const ADMIN_A = generateAci();
const INVITEE_A = generateAci();
const GROUP_ID = Bytes.toBase64(getRandomBytes(32));
const MASTER_KEY = Bytes.toBase64(getRandomBytes(32));
const PROFILEKEY = getRandomBytes(32);
// We need to eliminate fields that won't stay stable through import/export
function sortAndNormalize(
messages: Array<MessageAttributesType>
): Array<Partial<MessageAttributesType>> {
return sortBy(messages, 'sent_at').map(message =>
pick(
message,
'droppedGV2MemberIds',
'expirationTimerUpdate',
'groupMigration',
'groupV2Change',
'invitedGV2Members',
'sent_at',
'timestamp',
'type'
)
);
}
async function symmetricRoundtripHarness(
messages: Array<MessageAttributesType>
) {
return asymmetricRoundtripHarness(messages, messages);
}
async function asymmetricRoundtripHarness(
before: Array<MessageAttributesType>,
after: Array<MessageAttributesType>
) {
const outDir = mkdtempSync(path.join(tmpdir(), 'signal-temp-'));
try {
const targetOutputFile = path.join(outDir, 'backup.bin');
await Data.saveMessages(before, { forceSave: true, ourAci: OUR_ACI });
await backupsService.exportToDisk(targetOutputFile);
await clearData();
await backupsService.importBackup(() => createReadStream(targetOutputFile));
const messagesFromDatabase = await Data._getAllMessages();
const expected = sortAndNormalize(after);
const actual = sortAndNormalize(messagesFromDatabase);
assert.deepEqual(expected, actual);
} finally {
rmSync(outDir, { recursive: true });
}
}
async function clearData() {
await Data._removeAllMessages();
await Data._removeAllConversations();
await Data.removeAllItems();
window.storage.reset();
window.ConversationController.reset();
await setupBasics();
}
async function setupBasics() {
await window.storage.put('uuid_id', `${OUR_ACI}.2`);
await window.storage.put('pni', OUR_PNI);
await window.storage.put('masterKey', MASTER_KEY);
await window.storage.put('profileKey', PROFILEKEY);
await window.ConversationController.getOrCreateAndWait(OUR_ACI, 'private', {
pni: OUR_PNI,
systemGivenName: 'ME',
profileKey: Bytes.toBase64(PROFILEKEY),
});
}
let counter = 0;
function createMessage(
change: GroupV2ChangeType,
{ disableIncrement }: { disableIncrement: boolean } = {
disableIncrement: false,
}
): MessageAttributesType {
const groupConversation = window.ConversationController.get(GROUP_ID);
strictAssert(groupConversation, 'The group conversation must be created!');
if (!disableIncrement) {
counter += 1;
}
return {
conversationId: groupConversation.id,
groupV2Change: change,
id: generateGuid(),
received_at: counter,
sent_at: counter,
timestamp: counter,
type: 'group-v2-change',
};
}
describe('backup/groupv2/notifications', () => {
beforeEach(async () => {
await Data._removeAllMessages();
await Data._removeAllConversations();
window.storage.reset();
await setupBasics();
await window.ConversationController.getOrCreateAndWait(
CONTACT_A,
'private',
{ pni: CONTACT_A_PNI, systemGivenName: 'CONTACT_A' }
);
await window.ConversationController.getOrCreateAndWait(
CONTACT_B,
'private',
{ systemGivenName: 'CONTACT_B' }
);
await window.ConversationController.getOrCreateAndWait(
CONTACT_C,
'private',
{ systemGivenName: 'CONTACT_C' }
);
await window.ConversationController.getOrCreateAndWait(ADMIN_A, 'private', {
systemGivenName: 'ADMIN_A',
});
await window.ConversationController.getOrCreateAndWait(
INVITEE_A,
'private',
{
systemGivenName: 'INVITEE_A',
}
);
await window.ConversationController.getOrCreateAndWait(GROUP_ID, 'group', {
groupVersion: 2,
masterKey: Bytes.toBase64(getRandomBytes(32)),
name: 'Rock Enthusiasts',
});
await loadCallsHistory();
window.Events = {
...window.Events,
getTypingIndicatorSetting: () => false,
getLinkPreviewSetting: () => false,
};
});
describe('roundtrips given groupv2 notifications with', () => {
it('Multiple items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: CONTACT_A,
details: [
{
type: 'title',
newTitle: 'Saturday Running',
},
{
type: 'avatar',
removed: false,
},
{
type: 'description',
description:
'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
},
{
type: 'member-add',
aci: OUR_ACI,
},
{
type: 'description',
description: 'Another description',
},
{
type: 'member-privilege',
aci: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('Create items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'create',
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'create',
},
],
}),
createMessage({
details: [
{
type: 'create',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('Title items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'title',
newTitle: 'Saturday Running',
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'title',
newTitle: 'Saturday Running',
},
],
}),
createMessage({
details: [
{
type: 'title',
newTitle: 'Saturday Running',
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'title',
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'title',
},
],
}),
createMessage({
details: [
{
type: 'title',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('Avatar items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'avatar',
removed: false,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'avatar',
removed: false,
},
],
}),
createMessage({
details: [
{
type: 'avatar',
removed: false,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'avatar',
removed: true,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'avatar',
removed: true,
},
],
}),
createMessage({
details: [
{
type: 'avatar',
removed: true,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AccessAttributes items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
createMessage({
details: [
{
type: 'access-attributes',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AccessMembers items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
createMessage({
details: [
{
type: 'access-members',
newPrivilege: AccessControlEnum.MEMBER,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AccessInviteLink items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'access-invite-link',
newPrivilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('MemberAdd items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add',
aci: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add',
aci: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'member-add',
aci: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add',
aci: CONTACT_A,
},
],
}),
createMessage({
from: CONTACT_B,
details: [
{
type: 'member-add',
aci: CONTACT_A,
},
],
}),
createMessage({
details: [
{
type: 'member-add',
aci: CONTACT_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('MemberAddFromInvited items', async () => {
const messages: Array<MessageAttributesType> = [
// the strings where someone added you - shown like a normal add
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
inviter: CONTACT_B,
},
],
}),
createMessage({
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
inviter: CONTACT_A,
},
],
}),
// the rest of the 'someone added someone else' checks */
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
inviter: CONTACT_B,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_B,
inviter: CONTACT_C,
},
],
}),
createMessage({
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
inviter: CONTACT_B,
},
],
}),
// in all of these we know the user has accepted the invite
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
inviter: CONTACT_A,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
inviter: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
inviter: CONTACT_B,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
},
],
}),
// ACI accepts PNI invite (X joined the group)
// These don't roundtrip; the PNI from is replaced with ACI. See next test below.
// createMessage({
// from: OUR_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: OUR_ACI,
// pni: OUR_PNI,
// inviter: CONTACT_B,
// },
// ],
// }),
// createMessage({
// from: OUR_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: OUR_ACI,
// pni: OUR_PNI,
// },
// ],
// }),
// createMessage({
// from: CONTACT_A_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: CONTACT_A,
// pni: CONTACT_A_PNI,
// },
// ],
// }),
// ACI accepts PNI invite, the old way (X added X to group)
// These don't roundtrip; the PNI is replaced with ACI. See next test below.
// createMessage({
// from: OUR_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: OUR_ACI,
// inviter: CONTACT_B,
// },
// ],
// }),
// createMessage({
// from: OUR_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: OUR_ACI,
// },
// ],
// }),
// createMessage({
// from: CONTACT_A_PNI,
// details: [
// {
// type: 'member-add-from-invite',
// aci: CONTACT_A,
// },
// ],
// }),
];
await symmetricRoundtripHarness(messages);
});
it('MemberAddFromInvited items', async () => {
const firstBefore = createMessage({
from: OUR_PNI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
inviter: CONTACT_B,
},
],
});
const firstAfter = createMessage(
{
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
inviter: CONTACT_B,
},
],
},
{ disableIncrement: true }
);
const secondBefore = createMessage({
from: OUR_PNI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
},
],
});
const secondAfter = createMessage(
{
from: OUR_ACI,
details: [
{
type: 'member-add-from-invite',
aci: OUR_ACI,
},
],
},
{ disableIncrement: true }
);
const thirdBefore = createMessage({
from: CONTACT_A_PNI,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
},
],
});
const thirdAfter = createMessage(
{
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
},
],
},
{ disableIncrement: true }
);
const fourthBefore = createMessage({
from: CONTACT_A_PNI,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
pni: CONTACT_A_PNI,
},
],
});
const fourthAfter = createMessage(
{
from: CONTACT_A,
details: [
{
type: 'member-add-from-invite',
aci: CONTACT_A,
},
],
},
{ disableIncrement: true }
);
const before = [firstBefore, secondBefore, thirdBefore, fourthBefore];
const after = [firstAfter, secondAfter, thirdAfter, fourthAfter];
await asymmetricRoundtripHarness(before, after);
});
it('MemberAddFromLink items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add-from-link',
aci: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-add-from-link',
aci: CONTACT_A,
},
],
}),
// This doesn't roundtrip because if people join via link, they do it themselves.
// See the next test.
// createMessage({
// details: [
// {
// type: 'member-add-from-link',
// aci: CONTACT_A,
// },
// ],
// }),
];
await symmetricRoundtripHarness(messages);
});
it('MemberAddFromLink items asymmetric', async () => {
const before: Array<MessageAttributesType> = [
createMessage({
details: [
{
type: 'member-add-from-link',
aci: CONTACT_A,
},
],
}),
];
const after: Array<MessageAttributesType> = [
createMessage(
{
from: CONTACT_A,
details: [
{
type: 'member-add-from-link',
aci: CONTACT_A,
},
],
},
{ disableIncrement: true }
),
];
await asymmetricRoundtripHarness(before, after);
});
it('MemberAddFromAdminApproval items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: ADMIN_A,
details: [
{
type: 'member-add-from-admin-approval',
aci: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'member-add-from-admin-approval',
aci: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-add-from-admin-approval',
aci: CONTACT_A,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'member-add-from-admin-approval',
aci: CONTACT_A,
},
],
}),
createMessage({
details: [
{
type: 'member-add-from-admin-approval',
aci: CONTACT_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('MemberRemove items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-remove',
aci: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-remove',
aci: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'member-remove',
aci: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-remove',
aci: CONTACT_A,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-remove',
aci: CONTACT_A,
},
],
}),
createMessage({
from: CONTACT_B,
details: [
{
type: 'member-remove',
aci: CONTACT_A,
},
],
}),
createMessage({
details: [
{
type: 'member-remove',
aci: CONTACT_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('MemberPrivilege items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-privilege',
aci: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'member-privilege',
aci: OUR_ACI,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'member-privilege',
aci: OUR_ACI,
newPrivilege: RoleEnum.DEFAULT,
},
],
}),
createMessage({
details: [
{
type: 'member-privilege',
aci: OUR_ACI,
newPrivilege: RoleEnum.DEFAULT,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
}),
createMessage({
details: [
{
type: 'member-privilege',
aci: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('PendingAddOne items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-add-one',
serviceId: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'pending-add-one',
serviceId: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-add-one',
serviceId: INVITEE_A,
},
],
}),
// These don't roundtrip because we only have details if we're involved. See the
// next test.
// createMessage({
// from: CONTACT_B,
// details: [
// {
// type: 'pending-add-one',
// serviceId: INVITEE_A,
// },
// ],
// }),
// createMessage({
// details: [
// {
// type: 'pending-add-one',
// serviceId: INVITEE_A,
// },
// ],
// }),
];
await symmetricRoundtripHarness(messages);
});
it('PendingAddOne items, asymmetric', async () => {
const firstBefore = createMessage({
from: CONTACT_B,
details: [
{
type: 'pending-add-one',
serviceId: INVITEE_A,
},
],
});
const firstAfter = createMessage(
{
from: CONTACT_B,
details: [
{
type: 'pending-add-many',
count: 1,
},
],
},
{ disableIncrement: true }
);
const secondBefore = createMessage({
details: [
{
type: 'pending-add-one',
serviceId: INVITEE_A,
},
],
});
const secondAfter = createMessage(
{
details: [
{
type: 'pending-add-many',
count: 1,
},
],
},
{ disableIncrement: true }
);
const before = [firstBefore, secondBefore];
const after = [firstAfter, secondAfter];
await asymmetricRoundtripHarness(before, after);
});
it('PendingAddMany items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-add-many',
count: 5,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-add-many',
count: 1,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-add-many',
count: 5,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-add-many',
count: 1,
},
],
}),
createMessage({
details: [
{
type: 'pending-add-many',
count: 5,
},
],
}),
createMessage({
details: [
{
type: 'pending-add-many',
count: 1,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('PendingRemoveOne items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: INVITEE_A,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
inviter: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: INVITEE_A,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
},
],
}),
createMessage({
from: INVITEE_A,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
inviter: CONTACT_B,
},
],
}),
createMessage({
from: CONTACT_B,
details: [
{
type: 'pending-remove-one',
serviceId: OUR_ACI,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: CONTACT_B,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-remove-one',
serviceId: CONTACT_B,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: CONTACT_A,
},
],
}),
createMessage({
from: CONTACT_C,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: CONTACT_B,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: CONTACT_B,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
// Not roundtripped unless you were invited, or invitee said no to invite
// inviter: CONTACT_B,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
},
],
}),
createMessage({
from: CONTACT_B,
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-one',
serviceId: INVITEE_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('PendingRemoveMany items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 5,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 1,
// Inviter is not roundtripped for a multi-remove
// inviter: CONTACT_A,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 5,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'pending-remove-many',
count: 1,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-remove-many',
count: 5,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'pending-remove-many',
count: 1,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 5,
},
],
}),
createMessage({
details: [
{
type: 'pending-remove-many',
count: 1,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AdminApprovalAdd items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'admin-approval-add-one',
aci: OUR_ACI,
},
],
}),
createMessage({
from: CONTACT_A,
details: [
{
type: 'admin-approval-add-one',
aci: CONTACT_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AdminApprovalRemove items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'admin-approval-remove-one',
aci: OUR_ACI,
},
],
}),
createMessage({
details: [
{
type: 'admin-approval-remove-one',
aci: OUR_ACI,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'admin-approval-remove-one',
aci: CONTACT_A,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('AdminApprovalBounce items', async () => {
const messages: Array<MessageAttributesType> = [
// Should show button:
createMessage({
// From Joiner
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 1,
isApprovalPending: false,
},
],
}),
// These don't roundtrip, because we assume these always come from the requestor
// createMessage({
// // From nobody
// details: [
// {
// type: 'admin-approval-bounce',
// aci: CONTACT_A,
// times: 1,
// isApprovalPending: false,
// },
// ],
// }),
// createMessage({
// details: [
// {
// type: 'admin-approval-bounce',
// aci: CONTACT_A,
// times: 1,
// isApprovalPending: false,
// },
// // No group membership info
// ],
// }),
// Would show button, but we're not admin:
createMessage({
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 1,
isApprovalPending: false,
},
],
}),
// Would show button, but user is a group member:
createMessage({
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 1,
isApprovalPending: false,
},
],
}),
// Would show button, but user is already banned:
createMessage({
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 1,
isApprovalPending: false,
},
],
}),
// Open request
createMessage({
from: CONTACT_A,
details: [
{
type: 'admin-approval-bounce',
aci: CONTACT_A,
times: 4,
isApprovalPending: true,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('GroupLinkAdd items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ANY,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
createMessage({
details: [
{
type: 'group-link-add',
privilege: AccessControlEnum.ADMINISTRATOR,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('GroupLinkReset items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'group-link-reset',
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'group-link-reset',
},
],
}),
createMessage({
details: [
{
type: 'group-link-reset',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('GroupLinkRemove items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'group-link-remove',
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'group-link-remove',
},
],
}),
createMessage({
details: [
{
type: 'group-link-remove',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('DescriptionRemove items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
removed: true,
type: 'description',
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
removed: true,
type: 'description',
},
],
}),
createMessage({
details: [
{
removed: true,
type: 'description',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('DescriptionChange items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'description',
description:
'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'description',
description:
'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
},
],
}),
createMessage({
details: [
{
type: 'description',
description:
'This is a long description.\n\nWe need a dialog to view it all!\n\nIt has a link to https://example.com',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('DescriptionChange items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'announcements-only',
announcementsOnly: true,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'announcements-only',
announcementsOnly: true,
},
],
}),
createMessage({
details: [
{
type: 'announcements-only',
announcementsOnly: true,
},
],
}),
createMessage({
from: OUR_ACI,
details: [
{
type: 'announcements-only',
announcementsOnly: false,
},
],
}),
createMessage({
from: ADMIN_A,
details: [
{
type: 'announcements-only',
announcementsOnly: false,
},
],
}),
createMessage({
details: [
{
type: 'announcements-only',
announcementsOnly: false,
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
it('Summary items', async () => {
const messages: Array<MessageAttributesType> = [
createMessage({
from: OUR_ACI,
details: [
{
type: 'summary',
},
],
}),
];
await symmetricRoundtripHarness(messages);
});
});
describe('roundtrips given a timer change notification', () => {
it('in a group', async () => {
const groupConversation = window.ConversationController.get(GROUP_ID);
strictAssert(
groupConversation,
'The group conversation must be created!'
);
counter += 1;
const zeroTimer = {
id: generateGuid(),
conversationId: groupConversation.id,
expirationTimerUpdate: {
expireTimer: DurationInSeconds.fromSeconds(5),
sourceServiceId: CONTACT_A,
},
flags: EXPIRATION_TIMER_FLAG,
type: 'timer-notification' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
counter += 1;
const fiveSecondTimer = {
id: generateGuid(),
conversationId: groupConversation.id,
expirationTimerUpdate: {
expireTimer: DurationInSeconds.fromSeconds(5),
sourceServiceId: CONTACT_A,
},
flags: EXPIRATION_TIMER_FLAG,
type: 'timer-notification' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const messages: Array<MessageAttributesType> = [
zeroTimer,
fiveSecondTimer,
];
await symmetricRoundtripHarness(messages);
});
it('in a 1:1 conversation', async () => {
const contactA = window.ConversationController.get(CONTACT_A);
strictAssert(contactA, 'contactA conversation must be created!');
counter += 1;
const zeroTimer = {
id: generateGuid(),
conversationId: contactA.id,
expirationTimerUpdate: {
expireTimer: DurationInSeconds.fromSeconds(0),
sourceServiceId: CONTACT_A,
},
sourceServiceId: CONTACT_A,
flags: EXPIRATION_TIMER_FLAG,
type: 'timer-notification' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
counter += 1;
const fiveSecondTimer = {
id: generateGuid(),
conversationId: contactA.id,
expirationTimerUpdate: {
expireTimer: DurationInSeconds.fromSeconds(5),
sourceServiceId: OUR_ACI,
},
sourceServiceId: OUR_ACI,
flags: EXPIRATION_TIMER_FLAG,
type: 'timer-notification' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const messages: Array<MessageAttributesType> = [
zeroTimer,
fiveSecondTimer,
];
await symmetricRoundtripHarness(messages);
});
});
describe('roundtrips given migration notifications', () => {
it('symmetrically', async () => {
const groupConversation = window.ConversationController.get(GROUP_ID);
strictAssert(
groupConversation,
'The group conversation must be created!'
);
counter += 1;
const droppedOnly = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: false,
droppedMemberCount: 2,
invitedMemberCount: 0,
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
counter += 1;
const invitedOnly = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: false,
droppedMemberCount: 0,
invitedMemberCount: 1,
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
counter += 1;
const bothAndInvited = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: true,
droppedMemberCount: 2,
invitedMemberCount: 1,
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const messages: Array<MessageAttributesType> = [
droppedOnly,
invitedOnly,
bothAndInvited,
];
await symmetricRoundtripHarness(messages);
});
it('asymmetrically', async () => {
const groupConversation = window.ConversationController.get(GROUP_ID);
strictAssert(
groupConversation,
'The group conversation must be created!'
);
counter += 1;
const legacyBefore = {
id: generateGuid(),
conversationId: groupConversation.id,
droppedGV2MemberIds: [CONTACT_C],
invitedGV2Members: [
{ uuid: CONTACT_A, timestamp: counter, role: RoleEnum.DEFAULT },
{ uuid: CONTACT_B, timestamp: counter, role: RoleEnum.DEFAULT },
],
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const legacyAfter = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: false,
droppedMemberCount: 1,
invitedMemberCount: 2,
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
counter += 1;
const allDataBefore = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: true,
droppedMemberIds: [CONTACT_C],
invitedMembers: [
{ uuid: CONTACT_A, timestamp: counter, role: RoleEnum.DEFAULT },
{ uuid: CONTACT_B, timestamp: counter, role: RoleEnum.DEFAULT },
],
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const allDataAfter = {
id: generateGuid(),
conversationId: groupConversation.id,
groupMigration: {
areWeInvited: true,
droppedMemberCount: 1,
invitedMemberCount: 2,
},
type: 'group-v1-migration' as const,
received_at: counter,
sent_at: counter,
timestamp: counter,
};
const before = [legacyBefore, allDataBefore];
const after = [legacyAfter, allDataAfter];
await asymmetricRoundtripHarness(before, after);
});
});
});