Files
Desktop/ts/test-electron/sql/pollVoteMarkRead_test.preload.ts
2025-11-12 09:26:16 -06:00

641 lines
17 KiB
TypeScript

// Copyright 2025 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.preload.js';
import { generateAci } from '../../types/ServiceId.std.js';
import type { MessageAttributesType } from '../../model-types.d.ts';
import { postSaveUpdates } from '../../util/cleanup.preload.js';
const { _getAllMessages } = DataReader;
const {
_removeAllMessages,
saveMessages,
getUnreadPollVotesAndMarkRead,
markPollVoteAsRead,
} = DataWriter;
describe('sql/pollVoteMarkRead', () => {
beforeEach(async () => {
await _removeAllMessages();
});
describe('getUnreadPollVotesAndMarkRead', () => {
it('finds and marks unread poll votes in conversation', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage1: MessageAttributesType = {
id: generateUuid(),
body: 'poll 1',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test 1?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage2: MessageAttributesType = {
id: generateUuid(),
body: 'poll 2',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
hasUnreadPollVotes: true,
poll: {
question: 'Test 2?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage3: MessageAttributesType = {
id: generateUuid(),
body: 'poll 3',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 3,
received_at: start + 3,
timestamp: start + 3,
hasUnreadPollVotes: true,
poll: {
question: 'Test 3?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage1, pollMessage2, pollMessage3], {
forceSave: true,
ourAci,
postSaveUpdates,
});
assert.lengthOf(await _getAllMessages(), 3);
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: pollMessage2.received_at,
});
assert.lengthOf(markedRead, 2, 'two poll votes marked read');
// Verify correct messages were marked
const markedIds = markedRead.map(m => m.id);
assert.include(markedIds, pollMessage1.id);
assert.include(markedIds, pollMessage2.id);
// Verify they were actually marked read
const markedRead2 = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: pollMessage3.received_at,
});
assert.lengthOf(markedRead2, 1, 'only one poll vote remains unread');
assert.strictEqual(markedRead2[0].id, pollMessage3.id);
});
it('respects received_at cutoff', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage1: MessageAttributesType = {
id: generateUuid(),
body: 'poll 1',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test 1?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage2: MessageAttributesType = {
id: generateUuid(),
body: 'poll 2',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1000,
received_at: start + 1000,
timestamp: start + 1000,
hasUnreadPollVotes: true,
poll: {
question: 'Test 2?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage1, pollMessage2], {
forceSave: true,
ourAci,
postSaveUpdates,
});
// Only mark messages received before start + 500
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 500,
});
assert.lengthOf(markedRead, 1, 'only one poll vote within cutoff');
assert.strictEqual(markedRead[0].id, pollMessage1.id);
});
it('filters by conversationId correctly', async () => {
const start = Date.now();
const conversationId1 = generateUuid();
const conversationId2 = generateUuid();
const ourAci = generateAci();
const pollMessage1: MessageAttributesType = {
id: generateUuid(),
body: 'poll 1',
type: 'outgoing',
conversationId: conversationId1,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test 1?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage2: MessageAttributesType = {
id: generateUuid(),
body: 'poll 2',
type: 'outgoing',
conversationId: conversationId2,
sourceServiceId: ourAci,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
hasUnreadPollVotes: true,
poll: {
question: 'Test 2?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage1, pollMessage2], {
forceSave: true,
ourAci,
postSaveUpdates,
});
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId: conversationId1,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead, 1, 'only polls from conversationId1');
assert.strictEqual(markedRead[0].id, pollMessage1.id);
});
it('only returns messages with hasUnreadPollVotes = true', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage1: MessageAttributesType = {
id: generateUuid(),
body: 'poll 1',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test 1?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage2: MessageAttributesType = {
id: generateUuid(),
body: 'poll 2',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
hasUnreadPollVotes: false, // Already read
poll: {
question: 'Test 2?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage1, pollMessage2], {
forceSave: true,
ourAci,
postSaveUpdates,
});
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead, 1, 'only unread poll votes');
assert.strictEqual(markedRead[0].id, pollMessage1.id);
});
it('marks multiple poll votes as read in single call', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessages: Array<MessageAttributesType> = [];
for (let i = 0; i < 10; i += 1) {
pollMessages.push({
id: generateUuid(),
body: `poll ${i}`,
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + i,
received_at: start + i,
timestamp: start + i,
hasUnreadPollVotes: true,
poll: {
question: `Test ${i}?`,
options: [],
votes: [],
allowMultiple: false,
},
});
}
await saveMessages(pollMessages, {
forceSave: true,
ourAci,
postSaveUpdates,
});
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead, 10, 'all 10 polls marked read');
// Verify all were actually marked
const markedRead2 = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead2, 0, 'no unread polls remaining');
});
it('does not return already read poll votes on second call', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage: MessageAttributesType = {
id: generateUuid(),
body: 'poll',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage], {
forceSave: true,
ourAci,
postSaveUpdates,
});
// First call marks as read
const markedRead1 = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead1, 1);
// Second call should return empty
const markedRead2 = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead2, 0, 'idempotent - no polls on second call');
});
it('handles empty result set gracefully', async () => {
const conversationId = generateUuid();
const start = Date.now();
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start,
});
assert.isArray(markedRead);
assert.lengthOf(markedRead, 0);
});
});
describe('markPollVoteAsRead', () => {
it('finds and marks specific poll by author and timestamp', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage: MessageAttributesType = {
id: generateUuid(),
body: 'poll',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage], {
forceSave: true,
ourAci,
postSaveUpdates,
});
const result = await markPollVoteAsRead(pollMessage.sent_at);
assert.isDefined(result);
assert.strictEqual(result?.id, pollMessage.id);
// Verify it was marked read
const result2 = await markPollVoteAsRead(pollMessage.sent_at);
assert.isUndefined(
result2,
'should return undefined after already marked read'
);
});
it('returns undefined when no matching poll found', async () => {
const start = Date.now();
const result = await markPollVoteAsRead(start + 1);
assert.isUndefined(result);
});
it('returns undefined when poll already read', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage: MessageAttributesType = {
id: generateUuid(),
body: 'poll',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: false, // Already read
poll: {
question: 'Test?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage], {
forceSave: true,
ourAci,
postSaveUpdates,
});
const result = await markPollVoteAsRead(start + 1);
assert.isUndefined(result, 'should return undefined when already read');
});
it('marks only the specific poll, not others', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage1: MessageAttributesType = {
id: generateUuid(),
body: 'poll 1',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test 1?',
options: [],
votes: [],
allowMultiple: false,
},
};
const pollMessage2: MessageAttributesType = {
id: generateUuid(),
body: 'poll 2',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 2,
received_at: start + 2,
timestamp: start + 2,
hasUnreadPollVotes: true,
poll: {
question: 'Test 2?',
options: [],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage1, pollMessage2], {
forceSave: true,
ourAci,
postSaveUpdates,
});
// Mark only the first poll
const result = await markPollVoteAsRead(start + 1);
assert.isNotNull(result);
assert.strictEqual(result?.id, pollMessage1.id);
// Second poll should still be unread
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead, 1, 'second poll still unread');
assert.strictEqual(markedRead[0].id, pollMessage2.id);
});
it('returns full MessageAttributesType on success', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessage: MessageAttributesType = {
id: generateUuid(),
body: 'poll',
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
hasUnreadPollVotes: true,
poll: {
question: 'Test?',
options: ['Option 1'],
votes: [],
allowMultiple: false,
},
};
await saveMessages([pollMessage], {
forceSave: true,
ourAci,
postSaveUpdates,
});
const result = await markPollVoteAsRead(start + 1);
assert.isNotNull(result, 'result should not be null');
assert.strictEqual(result?.id, pollMessage.id, 'message id should match');
assert.strictEqual(result?.body, 'poll', 'message body should be "poll"');
assert.strictEqual(
result?.type,
'outgoing',
'message type should be "outgoing"'
);
assert.ok(
result?.hasUnreadPollVotes == null ||
result?.hasUnreadPollVotes === false,
'hasUnreadPollVotes should be false or null/undefined after marking as read'
);
assert.deepEqual(
result?.poll?.options,
['Option 1'],
'poll options should match'
);
});
it('handles multiple polls from same author', async () => {
const start = Date.now();
const conversationId = generateUuid();
const ourAci = generateAci();
const pollMessages: Array<MessageAttributesType> = [];
for (let i = 0; i < 5; i += 1) {
pollMessages.push({
id: generateUuid(),
body: `poll ${i}`,
type: 'outgoing',
conversationId,
sourceServiceId: ourAci,
sent_at: start + i * 1000,
received_at: start + i * 1000,
timestamp: start + i * 1000,
hasUnreadPollVotes: true,
poll: {
question: `Test ${i}?`,
options: [],
votes: [],
allowMultiple: false,
},
});
}
await saveMessages(pollMessages, {
forceSave: true,
ourAci,
postSaveUpdates,
});
// Mark specific poll by timestamp
const result = await markPollVoteAsRead(start + 2000);
assert.isNotNull(result);
assert.strictEqual(result?.id, pollMessages[2].id);
// Other polls should still be unread
const markedRead = await getUnreadPollVotesAndMarkRead({
conversationId,
readMessageReceivedAt: start + 10000,
});
assert.lengthOf(markedRead, 4, 'four polls still unread');
});
});
});