signal-desktop/ts/test-node/sql/migration_1060_test.ts
2024-05-28 11:56:00 -04:00

302 lines
7.6 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import type { Database } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3';
import { v4 as generateGuid } from 'uuid';
import {
getAllSyncTasksSync,
getMostRecentAddressableMessagesSync,
removeSyncTaskByIdSync,
saveSyncTasksSync,
} from '../../sql/Server';
import { insertData, updateToVersion } from './helpers';
import { MAX_SYNC_TASK_ATTEMPTS } from '../../util/syncTasks.types';
import { WEEK } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types';
import type { SyncTaskType } from '../../util/syncTasks';
/* eslint-disable camelcase */
function generateMessage(json: MessageAttributesType) {
const { conversationId, received_at, sent_at, type } = json;
return {
conversationId,
json,
received_at,
sent_at,
type,
};
}
describe('SQL/updateToSchemaVersion1060', () => {
let db: Database;
beforeEach(() => {
db = new SQL(':memory:');
updateToVersion(db, 1060);
});
afterEach(() => {
db.close();
});
describe('Addressable Messages', () => {
describe('Storing of new attachment jobs', () => {
it('returns only incoming/outgoing messages', () => {
const conversationId = generateGuid();
const otherConversationId = generateGuid();
insertData(db, 'messages', [
generateMessage({
id: '1',
conversationId,
type: 'incoming',
received_at: 1,
sent_at: 1,
timestamp: 1,
}),
generateMessage({
id: '2',
conversationId,
type: 'story',
received_at: 2,
sent_at: 2,
timestamp: 2,
}),
generateMessage({
id: '3',
conversationId,
type: 'outgoing',
received_at: 3,
sent_at: 3,
timestamp: 3,
}),
generateMessage({
id: '4',
conversationId,
type: 'group-v1-migration',
received_at: 4,
sent_at: 4,
timestamp: 4,
}),
generateMessage({
id: '5',
conversationId,
type: 'group-v2-change',
received_at: 5,
sent_at: 5,
timestamp: 5,
}),
generateMessage({
id: '6',
conversationId,
type: 'incoming',
received_at: 6,
sent_at: 6,
timestamp: 6,
}),
generateMessage({
id: '7',
conversationId,
type: 'profile-change',
received_at: 7,
sent_at: 7,
timestamp: 7,
}),
generateMessage({
id: '8',
conversationId: otherConversationId,
type: 'incoming',
received_at: 8,
sent_at: 8,
timestamp: 8,
}),
]);
const messages = getMostRecentAddressableMessagesSync(
db,
conversationId
);
assert.lengthOf(messages, 3);
assert.deepEqual(messages, [
{
id: '6',
conversationId,
type: 'incoming',
received_at: 6,
sent_at: 6,
timestamp: 6,
},
{
id: '3',
conversationId,
type: 'outgoing',
received_at: 3,
sent_at: 3,
timestamp: 3,
},
{
id: '1',
conversationId,
type: 'incoming',
received_at: 1,
sent_at: 1,
timestamp: 1,
},
]);
});
it('ensures that index is used for getMostRecentAddressableMessagesSync, with storyId', () => {
const { detail } = db
.prepare(
`
EXPLAIN QUERY PLAN
SELECT json FROM messages
INDEXED BY messages_by_date_addressable
WHERE
conversationId IS 'not-important' AND
isAddressableMessage = 1
ORDER BY received_at DESC, sent_at DESC
LIMIT 5;
`
)
.get();
assert.notInclude(detail, 'B-TREE');
assert.notInclude(detail, 'SCAN');
assert.include(
detail,
'SEARCH messages USING INDEX messages_by_date_addressable (conversationId=? AND isAddressableMessage=?)'
);
});
});
});
describe('Sync Tasks', () => {
it('creates tasks in bulk, and fetches all', () => {
const now = Date.now();
const expected: Array<SyncTaskType> = [
{
id: generateGuid(),
attempts: 1,
createdAt: now + 1,
data: {
jsonField: 'one',
data: 1,
},
envelopeId: 'envelope-id-1',
sentAt: 1,
type: 'delete-conversation',
},
{
id: generateGuid(),
attempts: 2,
createdAt: now + 2,
data: {
jsonField: 'two',
data: 2,
},
envelopeId: 'envelope-id-2',
sentAt: 2,
type: 'delete-conversation',
},
{
id: generateGuid(),
attempts: 3,
createdAt: now + 3,
data: {
jsonField: 'three',
data: 3,
},
envelopeId: 'envelope-id-3',
sentAt: 3,
type: 'delete-conversation',
},
];
saveSyncTasksSync(db, expected);
const actual = getAllSyncTasksSync(db);
assert.deepEqual(expected, actual, 'before delete');
removeSyncTaskByIdSync(db, expected[1].id);
const actualAfterDelete = getAllSyncTasksSync(db);
assert.deepEqual(
[
{ ...expected[0], attempts: 2 },
{ ...expected[2], attempts: 4 },
],
actualAfterDelete,
'after delete'
);
});
it('getAllSyncTasksSync expired tasks', () => {
const now = Date.now();
const twoWeeksAgo = now - WEEK * 2;
const expected: Array<SyncTaskType> = [
{
id: generateGuid(),
attempts: MAX_SYNC_TASK_ATTEMPTS,
createdAt: twoWeeksAgo,
data: {
jsonField: 'expired',
data: 1,
},
envelopeId: 'envelope-id-1',
sentAt: 1,
type: 'delete-conversation',
},
{
id: generateGuid(),
attempts: 2,
createdAt: twoWeeksAgo,
data: {
jsonField: 'old-but-few-attemts',
data: 2,
},
envelopeId: 'envelope-id-2',
sentAt: 2,
type: 'delete-conversation',
},
{
id: generateGuid(),
attempts: MAX_SYNC_TASK_ATTEMPTS * 2,
createdAt: now,
data: {
jsonField: 'new-but-many-attempts',
data: 3,
},
envelopeId: 'envelope-id-3',
sentAt: 3,
type: 'delete-conversation',
},
{
id: generateGuid(),
attempts: MAX_SYNC_TASK_ATTEMPTS - 1,
createdAt: now + 1,
data: {
jsonField: 'new-and-fresh',
data: 4,
},
envelopeId: 'envelope-id-4',
sentAt: 4,
type: 'delete-conversation',
},
];
saveSyncTasksSync(db, expected);
const actual = getAllSyncTasksSync(db);
assert.lengthOf(actual, 3);
assert.deepEqual([expected[1], expected[2], expected[3]], actual);
});
});
});