mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 10:19:08 +00:00
305 lines
9.8 KiB
TypeScript
305 lines
9.8 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
import assert from 'node:assert/strict';
|
|
import type { WritableDB } from '../../../sql/Interface.std.js';
|
|
import { setupTests } from '../../../sql/Server.node.js';
|
|
import type { AppendPinnedMessageResult } from '../../../sql/server/pinnedMessages.std.ts';
|
|
import {
|
|
appendPinnedMessage,
|
|
deletePinnedMessageByMessageId,
|
|
getNextExpiringPinnedMessageAcrossConversations,
|
|
deleteAllExpiredPinnedMessagesBefore,
|
|
} from '../../../sql/server/pinnedMessages.std.js';
|
|
import { createDB, insertData } from '../helpers.node.js';
|
|
import type {
|
|
PinnedMessage,
|
|
PinnedMessageParams,
|
|
} from '../../../types/PinnedMessage.std.js';
|
|
|
|
function setupData(db: WritableDB) {
|
|
insertData(db, 'conversations', [{ id: 'c1' }, { id: 'c2' }]);
|
|
insertData(db, 'messages', [
|
|
// conversation: c1
|
|
{ id: 'c1-m1', conversationId: 'c1' },
|
|
{ id: 'c1-m2', conversationId: 'c1' },
|
|
{ id: 'c1-m3', conversationId: 'c1' },
|
|
{ id: 'c1-m4', conversationId: 'c1' },
|
|
// conversation: c2
|
|
{ id: 'c2-m1', conversationId: 'c2' },
|
|
{ id: 'c2-m2', conversationId: 'c2' },
|
|
]);
|
|
}
|
|
|
|
function getParams(
|
|
conversationId: string,
|
|
messageId: string,
|
|
pinnedAt: number,
|
|
expiresAt: number | null = null
|
|
): PinnedMessageParams {
|
|
return {
|
|
messageId,
|
|
conversationId,
|
|
pinnedAt,
|
|
expiresAt,
|
|
};
|
|
}
|
|
|
|
describe('sql/server/pinnedMessages', () => {
|
|
let db: WritableDB;
|
|
|
|
beforeEach(() => {
|
|
db = createDB();
|
|
setupTests(db);
|
|
setupData(db);
|
|
});
|
|
|
|
afterEach(() => {
|
|
db.close();
|
|
});
|
|
|
|
function assertRows(expected: ReadonlyArray<PinnedMessage>) {
|
|
const rows = db.prepare('SELECT * FROM pinnedMessages').all();
|
|
assert.deepEqual(rows, expected);
|
|
}
|
|
|
|
function expectInserted(result: AppendPinnedMessageResult): PinnedMessage {
|
|
const inserted = result.change?.inserted;
|
|
assert(inserted != null, 'Append should have inserted a row');
|
|
return inserted;
|
|
}
|
|
|
|
describe('appendPinnedMessage', () => {
|
|
it('insert new pinned message', () => {
|
|
const params = getParams('c1', 'c1-m1', 1);
|
|
const result = appendPinnedMessage(db, 3, params);
|
|
const row = expectInserted(result);
|
|
assertRows([row]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 1, ...params },
|
|
replaced: null,
|
|
},
|
|
truncated: [],
|
|
});
|
|
});
|
|
|
|
it('replace existing pinned message', () => {
|
|
const initial = getParams('c1', 'c1-m1', 1);
|
|
const updated = getParams('c1', 'c1-m1', 2);
|
|
|
|
appendPinnedMessage(db, 3, initial);
|
|
const result = appendPinnedMessage(db, 3, updated);
|
|
const row = expectInserted(result);
|
|
assertRows([row]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 2, ...updated },
|
|
replaced: 1,
|
|
},
|
|
truncated: [],
|
|
});
|
|
});
|
|
|
|
it('truncates pinned messages to limit', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1);
|
|
const pin2 = getParams('c1', 'c1-m2', 2);
|
|
const pin3 = getParams('c1', 'c1-m3', 3);
|
|
const pin4 = getParams('c1', 'c1-m4', 4);
|
|
|
|
const row1 = expectInserted(appendPinnedMessage(db, 3, pin1));
|
|
const row2 = expectInserted(appendPinnedMessage(db, 3, pin2));
|
|
const row3 = expectInserted(appendPinnedMessage(db, 3, pin3));
|
|
assertRows([row1, row2, row3]);
|
|
|
|
const result = appendPinnedMessage(db, 3, pin4);
|
|
const row4 = expectInserted(result);
|
|
|
|
assertRows([row2, row3, row4]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 4, ...pin4 },
|
|
replaced: null,
|
|
},
|
|
truncated: [1],
|
|
});
|
|
});
|
|
|
|
it('doesnt truncate on top of replacing existing', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1);
|
|
const pin2 = getParams('c1', 'c1-m2', 2);
|
|
const pin3 = getParams('c1', 'c1-m3', 3);
|
|
const updated = { ...pin3, pinnedAt: 4 };
|
|
|
|
const row1 = expectInserted(appendPinnedMessage(db, 3, pin1));
|
|
const row2 = expectInserted(appendPinnedMessage(db, 3, pin2));
|
|
const row3 = expectInserted(appendPinnedMessage(db, 3, pin3));
|
|
assertRows([row1, row2, row3]);
|
|
|
|
const result = appendPinnedMessage(db, 3, updated);
|
|
const row4 = expectInserted(result);
|
|
assertRows([row1, row2, row4]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 4, ...updated },
|
|
replaced: 3,
|
|
},
|
|
truncated: [],
|
|
});
|
|
});
|
|
|
|
it('truncates multiple past limit', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1);
|
|
const pin2 = getParams('c1', 'c1-m2', 2);
|
|
const pin3 = getParams('c1', 'c1-m3', 3);
|
|
const pin4 = getParams('c1', 'c1-m4', 4);
|
|
|
|
let limit = 3;
|
|
|
|
const row1 = expectInserted(appendPinnedMessage(db, limit, pin1));
|
|
const row2 = expectInserted(appendPinnedMessage(db, limit, pin2));
|
|
const row3 = expectInserted(appendPinnedMessage(db, limit, pin3));
|
|
assertRows([row1, row2, row3]);
|
|
|
|
limit = 2;
|
|
|
|
const result = appendPinnedMessage(db, limit, pin4);
|
|
const row4 = expectInserted(result);
|
|
assertRows([row3, row4]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 4, ...pin4 },
|
|
replaced: null,
|
|
},
|
|
truncated: [1, 2],
|
|
});
|
|
});
|
|
|
|
it('truncates based on pinnedAt (not insert order) to handle out-of-order messages', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1);
|
|
const pin2 = getParams('c1', 'c1-m2', 2);
|
|
const pin3 = getParams('c1', 'c1-m3', 3);
|
|
const pin4 = getParams('c1', 'c1-m4', 3);
|
|
|
|
const row2 = expectInserted(appendPinnedMessage(db, 3, pin2));
|
|
const row3 = expectInserted(appendPinnedMessage(db, 3, pin3));
|
|
const row4 = expectInserted(appendPinnedMessage(db, 3, pin4));
|
|
const result = appendPinnedMessage(db, 3, pin1);
|
|
assertRows([row2, row3, row4]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
// Note: New row was immediately truncated
|
|
inserted: { id: 4, ...pin1 },
|
|
replaced: null,
|
|
},
|
|
truncated: [4],
|
|
});
|
|
});
|
|
|
|
it('should only truncate for the same conversation', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1);
|
|
const pin2 = getParams('c1', 'c1-m2', 2);
|
|
const pin3 = getParams('c1', 'c1-m3', 3);
|
|
const pin4 = getParams('c2', 'c2-m1', 4); // other chat
|
|
|
|
const row1 = expectInserted(appendPinnedMessage(db, 3, pin2));
|
|
const row2 = expectInserted(appendPinnedMessage(db, 3, pin3));
|
|
const row3 = expectInserted(appendPinnedMessage(db, 3, pin4));
|
|
const result = appendPinnedMessage(db, 3, pin1);
|
|
const row4 = expectInserted(result);
|
|
assertRows([row1, row2, row3, row4]);
|
|
|
|
assert.deepEqual(result, {
|
|
change: {
|
|
inserted: { id: 4, ...pin1 },
|
|
replaced: null,
|
|
},
|
|
truncated: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('deletePinnedMessageByMessageId', () => {
|
|
it('should return null if theres no matching pinned message', () => {
|
|
const result = deletePinnedMessageByMessageId(db, 'c1-m1');
|
|
assert.equal(result, null);
|
|
});
|
|
|
|
it('should return the deleted pinned message id', () => {
|
|
appendPinnedMessage(db, 3, getParams('c1', 'c1-m1', 1));
|
|
const result = deletePinnedMessageByMessageId(db, 'c1-m1');
|
|
assert.equal(result, 1);
|
|
});
|
|
});
|
|
|
|
describe('getNextExpiringPinnedMessageAcrossConversations', () => {
|
|
it('should return null if theres no pinned messages', () => {
|
|
const result = getNextExpiringPinnedMessageAcrossConversations(db);
|
|
assert.equal(result, null);
|
|
});
|
|
|
|
it('should return null if the pinned messages have no expiration', () => {
|
|
appendPinnedMessage(db, 3, getParams('c1', 'c1-m1', 1, null));
|
|
const result = getNextExpiringPinnedMessageAcrossConversations(db);
|
|
assert.equal(result, null);
|
|
});
|
|
|
|
it('should return the pinned message with the earliest expiration date', () => {
|
|
const pin1 = getParams('c1', 'c1-m1', 1, 4);
|
|
const pin2 = getParams('c1', 'c1-m1', 2, 3);
|
|
const pin3 = getParams('c2', 'c2-m1', 3, 2);
|
|
const pin4 = getParams('c2', 'c2-m2', 4, 1); // expires next
|
|
|
|
appendPinnedMessage(db, 3, pin1);
|
|
appendPinnedMessage(db, 3, pin2);
|
|
appendPinnedMessage(db, 3, pin3);
|
|
appendPinnedMessage(db, 3, pin4);
|
|
|
|
const result = getNextExpiringPinnedMessageAcrossConversations(db);
|
|
assert.deepEqual(result, {
|
|
id: 4,
|
|
...pin4,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('deleteAllExpiredPinnedMessagesBefore', () => {
|
|
function insertPin(params: PinnedMessageParams) {
|
|
return expectInserted(appendPinnedMessage(db, 3, params));
|
|
}
|
|
|
|
it('should return an empty array if theres no pinned messages', () => {
|
|
const result = deleteAllExpiredPinnedMessagesBefore(db, 1);
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
it('should not delete pinned messages that have no expiration', () => {
|
|
const row = insertPin(getParams('c1', 'c1-m1', 1, null)); // no expiration
|
|
const result = deleteAllExpiredPinnedMessagesBefore(db, 1);
|
|
assertRows([row]);
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
it('should not delete pinned messages that have not expired yet ', () => {
|
|
const row = insertPin(getParams('c1', 'c1-m1', 1, 2)); // not expired yet
|
|
const result = deleteAllExpiredPinnedMessagesBefore(db, 1);
|
|
assertRows([row]);
|
|
assert.deepEqual(result, []);
|
|
});
|
|
|
|
it('should delete pinned messages that have expired', () => {
|
|
const row1 = insertPin(getParams('c1', 'c1-m1', 1, 1)); // expired
|
|
const row2 = insertPin(getParams('c1', 'c1-m2', 2, 2)); // expired
|
|
const row3 = insertPin(getParams('c1', 'c1-m3', 3, 3)); // not expired yet
|
|
const result = deleteAllExpiredPinnedMessagesBefore(db, 2);
|
|
assertRows([row3]);
|
|
assert.deepEqual(result, [row1.id, row2.id]);
|
|
});
|
|
});
|
|
});
|