signal-desktop/ts/test-electron/sql/timelineFetches_test.ts
2024-07-22 11:16:33 -07:00

893 lines
24 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as generateUuid } from 'uuid';
import { DataReader, DataWriter } from '../../sql/Client';
import { generateAci } from '../../types/ServiceId';
import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus';
const {
_getAllMessages,
getMessageMetricsForConversation,
getNewerMessagesByConversation,
getOlderMessagesByConversation,
getTotalUnreadMentionsOfMeForConversation,
getOldestUnreadMentionOfMeForConversation,
} = DataReader;
const { removeAll, saveMessages } = DataWriter;
describe('sql/timelineFetches', () => {
beforeEach(async () => {
await removeAll();
});
describe('getOlderMessagesByConversation', () => {
it('returns N most recent messages', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const now = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId: generateUuid(),
sent_at: now,
received_at: now,
timestamp: now,
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 5);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: true,
limit: 5,
storyId: undefined,
});
assert.lengthOf(messages, 3);
// Fetched with DESC query, but with reverse() call afterwards
assert.strictEqual(messages[0].id, message1.id);
assert.strictEqual(messages[1].id, message2.id);
});
it('returns N most recent messages for a given story', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const now = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'story',
type: 'story',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
storyId,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'story reply 1',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'normal message',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
storyId,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message2.id);
});
it('returns N most recent messages excluding group story replies', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const now = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'story',
type: 'incoming',
conversationId,
sent_at: now - 20,
received_at: now - 20,
timestamp: now - 20,
storyId,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'story reply 1',
type: 'outgoing',
conversationId,
sent_at: now - 10,
received_at: now - 10,
timestamp: now - 10,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'normal message',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
storyId: undefined,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message3.id);
});
it('returns N messages older than provided received_at', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target + 10,
timestamp: target + 10,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
receivedAt: target,
sentAt: target,
storyId: undefined,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message1.id);
});
it('returns N older messages with received_at, lesser sent_at', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 20,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
receivedAt: target,
sentAt: target,
storyId: undefined,
});
assert.lengthOf(messages, 2);
// Fetched with DESC query, but with reverse() call afterwards
assert.strictEqual(messages[0].id, message1.id, 'checking message 1');
assert.strictEqual(messages[1].id, message2.id, 'checking message 2');
});
it('returns N older messages, same received_at/sent_at but excludes messageId', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getOlderMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
messageId: message2.id,
receivedAt: target,
sentAt: target,
storyId: undefined,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message1.id);
});
});
describe('getNewerMessagesByConversation', () => {
it('returns N oldest messages with no parameters', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const now = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId: generateUuid(),
sent_at: now,
received_at: now,
timestamp: now,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message4: MessageAttributesType = {
id: generateUuid(),
body: 'message 4',
type: 'outgoing',
conversationId,
sent_at: now + 10,
received_at: now + 10,
timestamp: now + 10,
};
const message5: MessageAttributesType = {
id: generateUuid(),
body: 'message 5',
type: 'outgoing',
conversationId,
sent_at: now + 20,
received_at: now + 20,
timestamp: now + 20,
};
await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 5);
const messages = await getNewerMessagesByConversation({
conversationId,
includeStoryReplies: true,
limit: 5,
storyId: undefined,
});
assert.lengthOf(messages, 3);
assert.strictEqual(messages[0].id, message3.id, 'checking message 3');
assert.strictEqual(messages[1].id, message4.id, 'checking message 4');
});
it('returns N oldest messages for a given story with no parameters', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const now = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'story',
conversationId,
sent_at: now,
received_at: now,
timestamp: now,
storyId,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: now + 10,
received_at: now + 10,
timestamp: now + 10,
storyId,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: now + 20,
received_at: now + 20,
timestamp: now + 20,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getNewerMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
storyId,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message2.id);
});
it('returns N messages newer than provided received_at', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target + 10,
timestamp: target + 10,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getNewerMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
receivedAt: target,
sentAt: target,
storyId: undefined,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message3.id);
});
it('returns N messages excluding group story replies', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
storyId: generateUuid(),
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target + 20,
received_at: target + 20,
timestamp: target + 20,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target + 10,
timestamp: target + 10,
storyId: generateUuid(),
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getNewerMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
storyId: undefined,
receivedAt: target,
sentAt: target,
});
assert.lengthOf(messages, 1);
assert.strictEqual(messages[0].id, message2.id);
});
it('returns N newer messages with same received_at, greater sent_at', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const message1: MessageAttributesType = {
id: generateUuid(),
body: 'message 1',
type: 'outgoing',
conversationId,
sent_at: target,
received_at: target,
timestamp: target,
};
const message2: MessageAttributesType = {
id: generateUuid(),
body: 'message 2',
type: 'outgoing',
conversationId,
sent_at: target + 10,
received_at: target,
timestamp: target,
};
const message3: MessageAttributesType = {
id: generateUuid(),
body: 'message 3',
type: 'outgoing',
conversationId,
sent_at: target + 20,
received_at: target,
timestamp: target,
};
await saveMessages([message1, message2, message3], {
forceSave: true,
ourAci,
});
assert.lengthOf(await _getAllMessages(), 3);
const messages = await getNewerMessagesByConversation({
conversationId,
includeStoryReplies: false,
limit: 5,
receivedAt: target,
sentAt: target,
storyId: undefined,
});
assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages[0].id, message2.id);
assert.strictEqual(messages[1].id, message3.id);
});
});
describe('getMessageMetricsForConversation', () => {
it('returns metrics properly for story and non-story timelines', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const storyId = generateUuid();
const ourAci = generateAci();
const story: MessageAttributesType = {
id: generateUuid(),
body: 'story',
type: 'story',
conversationId,
sent_at: target - 10,
received_at: target - 10,
timestamp: target - 10,
};
const oldestInStory: MessageAttributesType = {
id: generateUuid(),
body: 'oldestInStory',
type: 'outgoing',
conversationId,
sent_at: target - 9,
received_at: target - 9,
timestamp: target - 9,
storyId,
};
const oldest: MessageAttributesType = {
id: generateUuid(),
body: 'oldest',
type: 'outgoing',
conversationId,
sent_at: target - 8,
received_at: target - 8,
timestamp: target - 8,
};
const oldestUnseen: MessageAttributesType = {
id: generateUuid(),
body: 'oldestUnseen',
type: 'incoming',
conversationId,
sent_at: target - 7,
received_at: target - 7,
timestamp: target - 7,
readStatus: ReadStatus.Unread,
};
const oldestStoryUnread: MessageAttributesType = {
id: generateUuid(),
body: 'oldestStoryUnread',
type: 'incoming',
conversationId,
sent_at: target - 6,
received_at: target - 6,
timestamp: target - 6,
readStatus: ReadStatus.Unread,
storyId,
};
const anotherUnread: MessageAttributesType = {
id: generateUuid(),
body: 'anotherUnread',
type: 'incoming',
conversationId,
sent_at: target - 5,
received_at: target - 5,
timestamp: target - 5,
readStatus: ReadStatus.Unread,
};
const newestInStory: MessageAttributesType = {
id: generateUuid(),
body: 'newestStory',
type: 'outgoing',
conversationId,
sent_at: target - 4,
received_at: target - 4,
timestamp: target - 4,
storyId,
};
const newest: MessageAttributesType = {
id: generateUuid(),
body: 'newest',
type: 'outgoing',
conversationId,
sent_at: target - 3,
received_at: target - 3,
timestamp: target - 3,
};
await saveMessages(
[
story,
oldestInStory,
oldest,
oldestUnseen,
oldestStoryUnread,
anotherUnread,
newestInStory,
newest,
],
{ forceSave: true, ourAci }
);
assert.lengthOf(await _getAllMessages(), 8);
const metricsInTimeline = await getMessageMetricsForConversation({
conversationId,
includeStoryReplies: false,
});
assert.strictEqual(metricsInTimeline?.oldest?.id, oldest.id, 'oldest');
assert.strictEqual(metricsInTimeline?.newest?.id, newest.id, 'newest');
assert.strictEqual(
metricsInTimeline?.oldestUnseen?.id,
oldestUnseen.id,
'oldestUnseen'
);
assert.strictEqual(metricsInTimeline?.totalUnseen, 2, 'totalUnseen');
const metricsInStory = await getMessageMetricsForConversation({
conversationId,
storyId,
includeStoryReplies: true,
});
assert.strictEqual(
metricsInStory?.oldest?.id,
oldestInStory.id,
'oldestInStory'
);
assert.strictEqual(
metricsInStory?.newest?.id,
newestInStory.id,
'newestInStory'
);
assert.strictEqual(
metricsInStory?.oldestUnseen?.id,
oldestStoryUnread.id,
'oldestStoryUnread'
);
assert.strictEqual(metricsInStory?.totalUnseen, 1, 'totalUnseen');
});
});
describe('mentionsCount & oldestUnreadMention', () => {
it('returns unread mentions count and oldest unread mention', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const target = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const readMentionsMe: Partial<MessageAttributesType> = {
id: 'readMentionsMe',
readStatus: ReadStatus.Read,
mentionsMe: true,
};
const unreadMentionsMe: Partial<MessageAttributesType> = {
id: 'unreadMentionsMe',
readStatus: ReadStatus.Unread,
mentionsMe: true,
};
const unreadNoMention: Partial<MessageAttributesType> = {
id: 'unreadNoMention',
readStatus: ReadStatus.Unread,
};
const unreadMentionsMeAgain: Partial<MessageAttributesType> = {
id: 'unreadMentionsMeAgain',
readStatus: ReadStatus.Unread,
mentionsMe: true,
};
const messages = [
readMentionsMe,
unreadMentionsMe,
unreadNoMention,
unreadMentionsMeAgain,
];
const formattedMessages = messages.map<MessageAttributesType>(
(message, idx) => {
return {
id: generateUuid(),
body: 'body',
type: 'incoming',
sent_at: target - messages.length + idx,
received_at: target - messages.length + idx,
timestamp: target - messages.length + idx,
conversationId,
...message,
};
}
);
await saveMessages(formattedMessages, { forceSave: true, ourAci });
assert.lengthOf(await _getAllMessages(), 4);
const unreadMentions = await getTotalUnreadMentionsOfMeForConversation(
conversationId,
{ includeStoryReplies: false }
);
const oldestUnreadMention =
await getOldestUnreadMentionOfMeForConversation(conversationId, {
includeStoryReplies: false,
});
assert.strictEqual(unreadMentions, 2);
assert.strictEqual(oldestUnreadMention?.id, 'unreadMentionsMe');
});
});
});