// 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) { 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]); }); }); });