2021-04-28 18:36:10 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
|
|
|
|
|
|
import { assert } from 'chai';
|
2023-08-10 16:43:33 +00:00
|
|
|
import { v4 as generateUuid } from 'uuid';
|
2021-04-28 18:36:10 +00:00
|
|
|
import sinon from 'sinon';
|
|
|
|
import { ConversationModel } from '../models/conversations';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { ConversationAttributesType } from '../model-types.d';
|
2022-08-18 20:44:53 +00:00
|
|
|
import type { WebAPIType } from '../textsecure/WebAPI';
|
2023-08-10 16:43:33 +00:00
|
|
|
import { generateAci, normalizeServiceId } from '../types/ServiceId';
|
2021-04-28 18:36:10 +00:00
|
|
|
|
|
|
|
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
|
|
|
|
|
|
|
describe('updateConversationsWithUuidLookup', () => {
|
|
|
|
class FakeConversationController {
|
|
|
|
constructor(
|
|
|
|
private readonly conversations: Array<ConversationModel> = []
|
|
|
|
) {}
|
|
|
|
|
|
|
|
get(id?: string | null): ConversationModel | undefined {
|
|
|
|
return this.conversations.find(
|
|
|
|
conversation =>
|
|
|
|
conversation.id === id ||
|
|
|
|
conversation.get('e164') === id ||
|
|
|
|
conversation.get('uuid') === id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-09 21:39:00 +00:00
|
|
|
maybeMergeContacts({
|
2021-04-28 18:36:10 +00:00
|
|
|
e164,
|
2022-08-09 21:39:00 +00:00
|
|
|
aci: uuidFromServer,
|
|
|
|
reason,
|
2021-04-28 18:36:10 +00:00
|
|
|
}: {
|
|
|
|
e164?: string | null;
|
2022-08-09 21:39:00 +00:00
|
|
|
aci?: string | null;
|
|
|
|
reason?: string;
|
2022-12-05 22:46:54 +00:00
|
|
|
}): {
|
2023-02-01 21:32:46 +00:00
|
|
|
conversation: ConversationModel;
|
2022-12-05 22:46:54 +00:00
|
|
|
mergePromises: Array<Promise<void>>;
|
|
|
|
} {
|
2021-04-28 18:36:10 +00:00
|
|
|
assert(
|
|
|
|
e164,
|
|
|
|
'FakeConversationController is not set up for this case (E164 must be provided)'
|
|
|
|
);
|
|
|
|
assert(
|
2021-10-26 22:59:08 +00:00
|
|
|
uuidFromServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
'FakeConversationController is not set up for this case (UUID must be provided)'
|
|
|
|
);
|
|
|
|
assert(
|
2022-08-09 21:39:00 +00:00
|
|
|
reason,
|
|
|
|
'FakeConversationController must be provided a reason when merging'
|
2021-04-28 18:36:10 +00:00
|
|
|
);
|
2023-08-10 16:43:33 +00:00
|
|
|
const normalizedUuid = normalizeServiceId(uuidFromServer!, 'test');
|
2021-04-28 18:36:10 +00:00
|
|
|
|
|
|
|
const convoE164 = this.get(e164);
|
|
|
|
const convoUuid = this.get(normalizedUuid);
|
|
|
|
assert(
|
|
|
|
convoE164 || convoUuid,
|
|
|
|
'FakeConversationController is not set up for this case (at least one conversation should be found)'
|
|
|
|
);
|
|
|
|
|
|
|
|
if (convoE164 && convoUuid) {
|
|
|
|
if (convoE164 === convoUuid) {
|
2022-12-05 22:46:54 +00:00
|
|
|
return { conversation: convoUuid, mergePromises: [] };
|
2021-04-28 18:36:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
convoE164.unset('e164');
|
|
|
|
convoUuid.updateE164(e164);
|
2022-12-05 22:46:54 +00:00
|
|
|
return { conversation: convoUuid, mergePromises: [] };
|
2021-04-28 18:36:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (convoE164 && !convoUuid) {
|
|
|
|
convoE164.updateUuid(normalizedUuid);
|
2022-12-05 22:46:54 +00:00
|
|
|
return { conversation: convoE164, mergePromises: [] };
|
2022-08-09 21:39:00 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 21:32:46 +00:00
|
|
|
throw new Error('FakeConversationController should never get here');
|
2022-08-09 21:39:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
lookupOrCreate({
|
|
|
|
e164,
|
|
|
|
uuid: uuidFromServer,
|
|
|
|
}: {
|
|
|
|
e164?: string | null;
|
|
|
|
uuid?: string | null;
|
|
|
|
}): string | undefined {
|
|
|
|
assert(
|
|
|
|
e164,
|
|
|
|
'FakeConversationController is not set up for this case (E164 must be provided)'
|
|
|
|
);
|
|
|
|
assert(
|
|
|
|
uuidFromServer,
|
|
|
|
'FakeConversationController is not set up for this case (UUID must be provided)'
|
|
|
|
);
|
|
|
|
const normalizedUuid = uuidFromServer!.toLowerCase();
|
|
|
|
|
|
|
|
const convoE164 = this.get(e164);
|
|
|
|
const convoUuid = this.get(normalizedUuid);
|
|
|
|
assert(
|
|
|
|
convoE164 || convoUuid,
|
|
|
|
'FakeConversationController is not set up for this case (at least one conversation should be found)'
|
|
|
|
);
|
|
|
|
|
|
|
|
if (convoE164 && convoUuid) {
|
|
|
|
if (convoE164 === convoUuid) {
|
|
|
|
return convoUuid.get('id');
|
|
|
|
}
|
|
|
|
|
|
|
|
return convoUuid.get('id');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (convoE164 && !convoUuid) {
|
2021-04-28 18:36:10 +00:00
|
|
|
return convoE164.get('id');
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.fail('FakeConversationController should never get here');
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function createConversation(
|
|
|
|
attributes: Readonly<Partial<ConversationAttributesType>> = {}
|
|
|
|
): ConversationModel {
|
|
|
|
return new ConversationModel({
|
2023-08-10 16:43:33 +00:00
|
|
|
id: generateUuid(),
|
2021-04-28 18:36:10 +00:00
|
|
|
inbox_position: 0,
|
|
|
|
isPinned: false,
|
|
|
|
lastMessageDeletedForEveryone: false,
|
|
|
|
markedUnread: false,
|
|
|
|
messageCount: 1,
|
|
|
|
profileSharing: true,
|
|
|
|
sentMessageCount: 0,
|
|
|
|
type: 'private' as const,
|
|
|
|
version: 0,
|
|
|
|
...attributes,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let sinonSandbox: sinon.SinonSandbox;
|
|
|
|
|
2022-08-18 20:44:53 +00:00
|
|
|
let fakeCdsLookup: sinon.SinonStub;
|
2022-02-25 23:20:48 +00:00
|
|
|
let fakeCheckAccountExistence: sinon.SinonStub;
|
2022-08-18 20:44:53 +00:00
|
|
|
let fakeServer: Pick<WebAPIType, 'cdsLookup' | 'checkAccountExistence'>;
|
2021-04-28 18:36:10 +00:00
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
sinonSandbox = sinon.createSandbox();
|
|
|
|
|
|
|
|
sinonSandbox.stub(window.Signal.Data, 'updateConversation');
|
|
|
|
|
2022-08-18 20:44:53 +00:00
|
|
|
fakeCdsLookup = sinonSandbox.stub().resolves(new Map());
|
2022-02-25 23:20:48 +00:00
|
|
|
fakeCheckAccountExistence = sinonSandbox.stub().resolves(false);
|
2022-08-18 20:44:53 +00:00
|
|
|
fakeServer = {
|
|
|
|
cdsLookup: fakeCdsLookup,
|
2022-02-25 23:20:48 +00:00
|
|
|
checkAccountExistence: fakeCheckAccountExistence,
|
|
|
|
};
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
sinonSandbox.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does nothing when called with an empty array', async () => {
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController(),
|
|
|
|
conversations: [],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
2022-08-18 20:44:53 +00:00
|
|
|
sinon.assert.notCalled(fakeServer.cdsLookup as sinon.SinonStub);
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('does nothing when called with an array of conversations that lack E164s', async () => {
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController(),
|
|
|
|
conversations: [
|
|
|
|
createConversation(),
|
2023-08-10 16:43:33 +00:00
|
|
|
createConversation({ uuid: generateAci() }),
|
2021-04-28 18:36:10 +00:00
|
|
|
],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
2022-08-18 20:44:53 +00:00
|
|
|
sinon.assert.notCalled(fakeServer.cdsLookup as sinon.SinonStub);
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('updates conversations with their UUID', async () => {
|
|
|
|
const conversation1 = createConversation({ e164: '+13215559876' });
|
|
|
|
const conversation2 = createConversation({
|
|
|
|
e164: '+16545559876',
|
2023-08-10 16:43:33 +00:00
|
|
|
uuid: generateAci(), // should be overwritten
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
2023-08-10 16:43:33 +00:00
|
|
|
const aci1 = generateAci();
|
|
|
|
const aci2 = generateAci();
|
2021-04-28 18:36:10 +00:00
|
|
|
|
2022-08-18 20:44:53 +00:00
|
|
|
fakeCdsLookup.resolves(
|
|
|
|
new Map([
|
2023-08-10 16:43:33 +00:00
|
|
|
['+13215559876', { aci: aci1, pni: undefined }],
|
|
|
|
['+16545559876', { aci: aci2, pni: undefined }],
|
2022-08-18 20:44:53 +00:00
|
|
|
])
|
|
|
|
);
|
2021-04-28 18:36:10 +00:00
|
|
|
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController([
|
|
|
|
conversation1,
|
|
|
|
conversation2,
|
|
|
|
]),
|
|
|
|
conversations: [conversation1, conversation2],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
2023-08-10 16:43:33 +00:00
|
|
|
assert.strictEqual(conversation1.get('uuid'), aci1);
|
|
|
|
assert.strictEqual(conversation2.get('uuid'), aci2);
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("marks conversations unregistered if we didn't have a UUID for them and the server also doesn't have one", async () => {
|
|
|
|
const conversation = createConversation({ e164: '+13215559876' });
|
|
|
|
assert.isUndefined(
|
|
|
|
conversation.get('discoveredUnregisteredAt'),
|
|
|
|
'Test was not set up correctly'
|
|
|
|
);
|
|
|
|
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController([conversation]),
|
|
|
|
conversations: [conversation],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
assert.approximately(
|
|
|
|
conversation.get('discoveredUnregisteredAt') || 0,
|
|
|
|
Date.now(),
|
|
|
|
5000
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2022-02-25 23:20:48 +00:00
|
|
|
it("doesn't mark conversations unregistered if we already had a UUID for them, even if the account exists on server", async () => {
|
2023-08-10 16:43:33 +00:00
|
|
|
const existingServiceId = generateAci();
|
2021-04-28 18:36:10 +00:00
|
|
|
const conversation = createConversation({
|
|
|
|
e164: '+13215559876',
|
2023-08-10 16:43:33 +00:00
|
|
|
uuid: existingServiceId,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
assert.isUndefined(
|
|
|
|
conversation.get('discoveredUnregisteredAt'),
|
|
|
|
'Test was not set up correctly'
|
|
|
|
);
|
|
|
|
|
2022-02-25 23:20:48 +00:00
|
|
|
fakeCheckAccountExistence.resolves(true);
|
2021-04-28 18:36:10 +00:00
|
|
|
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController([conversation]),
|
|
|
|
conversations: [conversation],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|
|
|
|
|
2023-08-10 16:43:33 +00:00
|
|
|
assert.strictEqual(conversation.get('uuid'), existingServiceId);
|
2021-04-28 18:36:10 +00:00
|
|
|
assert.isUndefined(conversation.get('discoveredUnregisteredAt'));
|
|
|
|
});
|
2022-02-25 23:20:48 +00:00
|
|
|
|
2022-08-09 21:39:00 +00:00
|
|
|
it('marks conversations unregistered and removes UUID if the account does not exist on server', async () => {
|
2023-08-10 16:43:33 +00:00
|
|
|
const existingServiceId = generateAci();
|
2022-02-25 23:20:48 +00:00
|
|
|
const conversation = createConversation({
|
|
|
|
e164: '+13215559876',
|
2023-08-10 16:43:33 +00:00
|
|
|
uuid: existingServiceId,
|
2022-02-25 23:20:48 +00:00
|
|
|
});
|
|
|
|
assert.isUndefined(
|
|
|
|
conversation.get('discoveredUnregisteredAt'),
|
|
|
|
'Test was not set up correctly'
|
|
|
|
);
|
|
|
|
|
|
|
|
fakeCheckAccountExistence.resolves(false);
|
|
|
|
|
|
|
|
await updateConversationsWithUuidLookup({
|
|
|
|
conversationController: new FakeConversationController([conversation]),
|
|
|
|
conversations: [conversation],
|
2022-08-18 20:44:53 +00:00
|
|
|
server: fakeServer,
|
2022-02-25 23:20:48 +00:00
|
|
|
});
|
|
|
|
|
2022-08-09 21:39:00 +00:00
|
|
|
assert.isUndefined(conversation.get('uuid'));
|
2022-02-25 23:20:48 +00:00
|
|
|
assert.isNumber(conversation.get('discoveredUnregisteredAt'));
|
|
|
|
});
|
2021-04-28 18:36:10 +00:00
|
|
|
});
|