LibreChat/packages/data-schemas/src/methods/share.test.ts

1044 lines
32 KiB
TypeScript
Raw Normal View History

import { nanoid } from 'nanoid';
import mongoose from 'mongoose';
import { Constants } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { createShareMethods, type ShareMethods } from './share';
import type { SchemaWithMeiliMethods } from '~/models/plugins/mongoMeili';
import type * as t from '~/types';
describe('Share Methods', () => {
let mongoServer: MongoMemoryServer;
let shareMethods: ShareMethods;
let SharedLink: mongoose.Model<t.ISharedLink>;
let Message: mongoose.Model<t.IMessage>;
let Conversation: SchemaWithMeiliMethods;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
// Create schemas
const sharedLinkSchema = new mongoose.Schema<t.ISharedLink>(
{
conversationId: { type: String, required: true },
title: { type: String, index: true },
user: { type: String, index: true },
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
shareId: { type: String, index: true },
isPublic: { type: Boolean, default: true },
},
{ timestamps: true },
);
const messageSchema = new mongoose.Schema<t.IMessage>(
{
messageId: { type: String, required: true },
conversationId: { type: String, required: true },
user: { type: String, required: true },
text: String,
isCreatedByUser: Boolean,
model: String,
parentMessageId: String,
attachments: [mongoose.Schema.Types.Mixed],
content: [mongoose.Schema.Types.Mixed],
},
{ timestamps: true },
);
const conversationSchema = new mongoose.Schema<t.IConversation>(
{
conversationId: { type: String, required: true },
title: String,
user: String,
},
{ timestamps: true },
);
// Register models
SharedLink =
mongoose.models.SharedLink || mongoose.model<t.ISharedLink>('SharedLink', sharedLinkSchema);
Message = mongoose.models.Message || mongoose.model<t.IMessage>('Message', messageSchema);
Conversation = (mongoose.models.Conversation ||
mongoose.model<t.IConversation>(
'Conversation',
conversationSchema,
)) as SchemaWithMeiliMethods;
// Create share methods
shareMethods = createShareMethods(mongoose);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await SharedLink.deleteMany({});
await Message.deleteMany({});
await Conversation.deleteMany({});
});
describe('createSharedLink', () => {
test('should create a new shared link', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
// Create test conversation
await Conversation.create({
conversationId,
title: 'Test Conversation',
user: userId,
});
// Create test messages
await Message.create([
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Hello',
isCreatedByUser: true,
},
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'World',
isCreatedByUser: false,
model: 'gpt-4',
},
]);
const result = await shareMethods.createSharedLink(userId, conversationId);
expect(result).toBeDefined();
expect(result.shareId).toBeDefined();
expect(result.conversationId).toBe(conversationId);
// Verify the share was created in the database
const savedShare = await SharedLink.findOne({ shareId: result.shareId });
expect(savedShare).toBeDefined();
expect(savedShare?.user).toBe(userId);
expect(savedShare?.title).toBe('Test Conversation');
expect(savedShare?.messages).toHaveLength(2);
});
test('should throw error if share already exists', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
await Conversation.create({
conversationId,
title: 'Test Conversation',
user: userId,
});
// Create messages so we can create a share
await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Test message',
isCreatedByUser: true,
});
// Create first share
await shareMethods.createSharedLink(userId, conversationId);
// Try to create duplicate
await expect(shareMethods.createSharedLink(userId, conversationId)).rejects.toThrow(
'Share already exists',
);
});
test('should throw error with missing parameters', async () => {
await expect(shareMethods.createSharedLink('', 'conv123')).rejects.toThrow(
'Missing required parameters',
);
await expect(shareMethods.createSharedLink('user123', '')).rejects.toThrow(
'Missing required parameters',
);
});
test('should only include messages from the same user', async () => {
const userId1 = new mongoose.Types.ObjectId().toString();
const userId2 = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
await Conversation.create({
conversationId,
title: 'Test Conversation',
user: userId1,
});
// Create messages from different users
await Message.create([
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId1,
text: 'User 1 message',
isCreatedByUser: true,
},
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId2,
text: 'User 2 message',
isCreatedByUser: true,
},
]);
const result = await shareMethods.createSharedLink(userId1, conversationId);
const savedShare = await SharedLink.findOne({ shareId: result.shareId }).populate('messages');
expect(savedShare?.messages).toHaveLength(1);
expect((savedShare?.messages?.[0] as unknown as t.IMessage | undefined)?.text).toBe(
'User 1 message',
);
});
test('should not allow user to create shared link for conversation they do not own', async () => {
const ownerUserId = new mongoose.Types.ObjectId().toString();
const otherUserId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
// Create conversation owned by ownerUserId
await Conversation.create({
conversationId,
title: 'Owner Conversation',
user: ownerUserId,
});
// Create messages for the conversation
await Message.create([
{
messageId: `msg_${nanoid()}`,
conversationId,
user: ownerUserId,
text: 'Owner message',
isCreatedByUser: true,
},
]);
// Try to create a shared link as a different user
await expect(shareMethods.createSharedLink(otherUserId, conversationId)).rejects.toThrow(
'Conversation not found or access denied',
);
// Verify no share was created
const shares = await SharedLink.find({ conversationId });
expect(shares).toHaveLength(0);
});
test('should not allow creating share for conversation with no messages', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
// Create conversation without any messages
await Conversation.create({
conversationId,
title: 'Empty Conversation',
user: userId,
});
// Try to create a shared link for conversation with no messages
await expect(shareMethods.createSharedLink(userId, conversationId)).rejects.toThrow(
'No messages to share',
);
// Verify no share was created
const shares = await SharedLink.find({ conversationId });
expect(shares).toHaveLength(0);
});
});
describe('getSharedMessages', () => {
test('should retrieve and anonymize shared messages', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
// Create messages
const messages = await Message.create([
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Hello',
isCreatedByUser: true,
parentMessageId: Constants.NO_PARENT,
},
{
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'World',
isCreatedByUser: false,
model: 'gpt-4',
parentMessageId: Constants.NO_PARENT,
},
]);
// Create shared link
await SharedLink.create({
shareId,
conversationId,
user: userId,
title: 'Test Share',
messages: messages.map((m) => m._id),
isPublic: true,
});
const result = await shareMethods.getSharedMessages(shareId);
expect(result).toBeDefined();
expect(result?.shareId).toBe(shareId);
expect(result?.conversationId).not.toBe(conversationId); // Should be anonymized
expect(result?.messages).toHaveLength(2);
// Check anonymization
result?.messages.forEach((msg) => {
expect(msg.messageId).toMatch(/^msg_/); // Should be anonymized with msg_ prefix
expect(msg.messageId).not.toBe(messages[0].messageId); // Should be different from original
expect(msg.conversationId).toBe(result.conversationId);
expect(msg.user).toBeUndefined(); // User should be removed
});
});
test('should return null for non-public share', async () => {
const shareId = `share_${nanoid()}`;
await SharedLink.create({
shareId,
conversationId: 'conv123',
user: 'user123',
isPublic: false,
});
const result = await shareMethods.getSharedMessages(shareId);
expect(result).toBeNull();
});
test('should return null for non-existent share', async () => {
const result = await shareMethods.getSharedMessages('non_existent_share');
expect(result).toBeNull();
});
test('should handle messages with attachments', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const message = await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Message with attachment',
isCreatedByUser: true,
attachments: [
{
file_id: 'file123',
filename: 'test.pdf',
type: 'application/pdf',
},
],
});
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: [message._id],
isPublic: true,
});
const result = await shareMethods.getSharedMessages(shareId);
expect(result?.messages[0].attachments).toHaveLength(1);
expect(
(result?.messages[0].attachments?.[0] as unknown as t.IMessage | undefined)?.messageId,
).toBe(result?.messages[0].messageId);
expect(
(result?.messages[0].attachments?.[0] as unknown as t.IMessage | undefined)?.conversationId,
).toBe(result?.conversationId);
});
});
describe('getSharedLinks', () => {
test('should retrieve paginated shared links for a user', async () => {
const userId = new mongoose.Types.ObjectId().toString();
// Create multiple shared links
const sharePromises = Array.from({ length: 15 }, (_, i) =>
SharedLink.create({
shareId: `share_${i}`,
conversationId: `conv_${i}`,
user: userId,
title: `Share ${i}`,
isPublic: true,
createdAt: new Date(Date.now() - i * 1000 * 60), // Different timestamps
}),
);
await Promise.all(sharePromises);
const result = await shareMethods.getSharedLinks(userId, undefined, 10);
expect(result.links).toHaveLength(10);
expect(result.hasNextPage).toBe(true);
expect(result.nextCursor).toBeDefined();
// Check ordering (newest first by default)
expect(result.links[0].title).toBe('Share 0');
expect(result.links[9].title).toBe('Share 9');
});
test('should filter by isPublic parameter', async () => {
const userId = new mongoose.Types.ObjectId().toString();
await SharedLink.create([
{
shareId: 'public_share',
conversationId: 'conv1',
user: userId,
title: 'Public Share',
isPublic: true,
},
{
shareId: 'private_share',
conversationId: 'conv2',
user: userId,
title: 'Private Share',
isPublic: false,
},
]);
const publicResults = await shareMethods.getSharedLinks(userId, undefined, 10, true);
const privateResults = await shareMethods.getSharedLinks(userId, undefined, 10, false);
expect(publicResults.links).toHaveLength(1);
expect(publicResults.links[0].title).toBe('Public Share');
expect(privateResults.links).toHaveLength(1);
expect(privateResults.links[0].title).toBe('Private Share');
});
test('should handle search with mocked meiliSearch', async () => {
const userId = new mongoose.Types.ObjectId().toString();
// Mock meiliSearch method
Conversation.meiliSearch = jest.fn().mockResolvedValue({
hits: [{ conversationId: 'conv1' }],
});
await SharedLink.create([
{
shareId: 'share1',
conversationId: 'conv1',
user: userId,
title: 'Matching Share',
isPublic: true,
},
{
shareId: 'share2',
conversationId: 'conv2',
user: userId,
title: 'Non-matching Share',
isPublic: true,
},
]);
const result = await shareMethods.getSharedLinks(
userId,
undefined,
10,
true,
'createdAt',
'desc',
'search term',
);
expect(result.links).toHaveLength(1);
expect(result.links[0].title).toBe('Matching Share');
});
test('should handle empty results', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const result = await shareMethods.getSharedLinks(userId);
expect(result.links).toHaveLength(0);
expect(result.hasNextPage).toBe(false);
expect(result.nextCursor).toBeUndefined();
});
test('should only return shares for the specified user', async () => {
const userId1 = new mongoose.Types.ObjectId().toString();
const userId2 = new mongoose.Types.ObjectId().toString();
// Create shares for different users
await SharedLink.create([
{
shareId: 'share1',
conversationId: 'conv1',
user: userId1,
title: 'User 1 Share',
isPublic: true,
},
{
shareId: 'share2',
conversationId: 'conv2',
user: userId2,
title: 'User 2 Share',
isPublic: true,
},
{
shareId: 'share3',
conversationId: 'conv3',
user: userId1,
title: 'Another User 1 Share',
isPublic: true,
},
]);
const result1 = await shareMethods.getSharedLinks(userId1);
const result2 = await shareMethods.getSharedLinks(userId2);
expect(result1.links).toHaveLength(2);
expect(result1.links.every((link) => link.title.includes('User 1'))).toBe(true);
expect(result2.links).toHaveLength(1);
expect(result2.links[0].title).toBe('User 2 Share');
});
});
describe('updateSharedLink', () => {
test('should update shared link with new messages', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const oldShareId = `share_${nanoid()}`;
// Create initial messages
const initialMessages = await Message.create([
{
messageId: `msg_1`,
conversationId,
user: userId,
text: 'Initial message',
isCreatedByUser: true,
},
]);
// Create shared link
await SharedLink.create({
shareId: oldShareId,
conversationId,
user: userId,
messages: initialMessages.map((m) => m._id),
isPublic: true,
});
// Add new message
await Message.create({
messageId: `msg_2`,
conversationId,
user: userId,
text: 'New message',
isCreatedByUser: false,
});
const result = await shareMethods.updateSharedLink(userId, oldShareId);
expect(result.shareId).not.toBe(oldShareId); // Should generate new shareId
expect(result.conversationId).toBe(conversationId);
// Verify updated share
const updatedShare = await SharedLink.findOne({ shareId: result.shareId }).populate(
'messages',
);
expect(updatedShare?.messages).toHaveLength(2);
});
test('should throw error if share not found', async () => {
await expect(shareMethods.updateSharedLink('user123', 'non_existent')).rejects.toThrow(
'Share not found',
);
});
test('should throw error with missing parameters', async () => {
await expect(shareMethods.updateSharedLink('', 'share123')).rejects.toThrow(
'Missing required parameters',
);
});
test('should only update with messages from the same user', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const otherUserId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
// Create initial share
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: [],
isPublic: true,
});
// Add messages from different users
await Message.create([
{
messageId: `msg_1`,
conversationId,
user: userId,
text: 'User message',
isCreatedByUser: true,
},
{
messageId: `msg_2`,
conversationId,
user: otherUserId,
text: 'Other user message',
isCreatedByUser: true,
},
]);
const result = await shareMethods.updateSharedLink(userId, shareId);
const updatedShare = await SharedLink.findOne({ shareId: result.shareId }).populate(
'messages',
);
expect(updatedShare?.messages).toHaveLength(1);
expect((updatedShare?.messages?.[0] as unknown as t.IMessage | undefined)?.text).toBe(
'User message',
);
});
test('should not allow user to update shared link they do not own', async () => {
const ownerUserId = new mongoose.Types.ObjectId().toString();
const otherUserId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
// Create shared link owned by ownerUserId
await SharedLink.create({
shareId,
conversationId,
user: ownerUserId,
messages: [],
isPublic: true,
});
// Try to update as a different user
await expect(shareMethods.updateSharedLink(otherUserId, shareId)).rejects.toThrow(
'Share not found',
);
// Verify the original share still exists and is unchanged
const originalShare = await SharedLink.findOne({ shareId });
expect(originalShare).toBeDefined();
expect(originalShare?.user).toBe(ownerUserId);
});
});
describe('deleteSharedLink', () => {
test('should delete shared link', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const shareId = `share_${nanoid()}`;
await SharedLink.create({
shareId,
conversationId: 'conv123',
user: userId,
isPublic: true,
});
const result = await shareMethods.deleteSharedLink(userId, shareId);
expect(result).toBeDefined();
expect(result?.success).toBe(true);
expect(result?.shareId).toBe(shareId);
// Verify deletion
const deletedShare = await SharedLink.findOne({ shareId });
expect(deletedShare).toBeNull();
});
test('should return null if share not found', async () => {
const result = await shareMethods.deleteSharedLink('user123', 'non_existent');
expect(result).toBeNull();
});
test('should not delete share from different user', async () => {
const userId1 = new mongoose.Types.ObjectId().toString();
const userId2 = new mongoose.Types.ObjectId().toString();
const shareId = `share_${nanoid()}`;
await SharedLink.create({
shareId,
conversationId: 'conv123',
user: userId1,
isPublic: true,
});
const result = await shareMethods.deleteSharedLink(userId2, shareId);
expect(result).toBeNull();
// Verify share still exists
const share = await SharedLink.findOne({ shareId });
expect(share).toBeDefined();
});
test('should handle missing parameters for deleteSharedLink', async () => {
await expect(shareMethods.deleteSharedLink('', 'share123')).rejects.toThrow(
'Missing required parameters',
);
await expect(shareMethods.deleteSharedLink('user123', '')).rejects.toThrow(
'Missing required parameters',
);
});
});
describe('getSharedLink', () => {
test('should retrieve existing shared link', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
await SharedLink.create({
shareId,
conversationId,
user: userId,
isPublic: true,
});
const result = await shareMethods.getSharedLink(userId, conversationId);
expect(result.success).toBe(true);
expect(result.shareId).toBe(shareId);
});
test('should return null shareId if not found', async () => {
const result = await shareMethods.getSharedLink('user123', 'conv123');
expect(result.success).toBe(false);
expect(result.shareId).toBeNull();
});
test('should not return share from different user', async () => {
const userId1 = new mongoose.Types.ObjectId().toString();
const userId2 = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
await SharedLink.create({
shareId: 'share123',
conversationId,
user: userId1,
isPublic: true,
});
const result = await shareMethods.getSharedLink(userId2, conversationId);
expect(result.success).toBe(false);
expect(result.shareId).toBeNull();
});
test('should handle missing parameters for getSharedLink', async () => {
await expect(shareMethods.getSharedLink('', 'conv123')).rejects.toThrow(
'Missing required parameters',
);
await expect(shareMethods.getSharedLink('user123', '')).rejects.toThrow(
'Missing required parameters',
);
});
test('should only return public shares', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
// Create a non-public share
await SharedLink.create({
shareId,
conversationId,
user: userId,
isPublic: false,
});
const result = await shareMethods.getSharedLink(userId, conversationId);
expect(result.success).toBe(false);
expect(result.shareId).toBeNull();
});
});
describe('deleteAllSharedLinks', () => {
test('should delete all shared links for a user', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const otherUserId = new mongoose.Types.ObjectId().toString();
// Create shares for different users
await SharedLink.create([
{ shareId: 'share1', conversationId: 'conv1', user: userId },
{ shareId: 'share2', conversationId: 'conv2', user: userId },
{ shareId: 'share3', conversationId: 'conv3', user: otherUserId },
]);
const result = await shareMethods.deleteAllSharedLinks(userId);
expect(result.deletedCount).toBe(2);
expect(result.message).toContain('successfully');
// Verify only user's shares were deleted
const remainingShares = await SharedLink.find({});
expect(remainingShares).toHaveLength(1);
expect(remainingShares[0].user).toBe(otherUserId);
});
test('should handle when no shares exist', async () => {
const result = await shareMethods.deleteAllSharedLinks('user123');
expect(result.deletedCount).toBe(0);
expect(result.message).toContain('successfully');
});
test('should only delete shares belonging to the specified user', async () => {
const userId1 = new mongoose.Types.ObjectId().toString();
const userId2 = new mongoose.Types.ObjectId().toString();
const userId3 = new mongoose.Types.ObjectId().toString();
// Create multiple shares for different users
await SharedLink.create([
{ shareId: 'share1', conversationId: 'conv1', user: userId1, isPublic: true },
{ shareId: 'share2', conversationId: 'conv2', user: userId1, isPublic: false },
{ shareId: 'share3', conversationId: 'conv3', user: userId2, isPublic: true },
{ shareId: 'share4', conversationId: 'conv4', user: userId2, isPublic: true },
{ shareId: 'share5', conversationId: 'conv5', user: userId3, isPublic: true },
]);
// Delete all shares for userId1
const result = await shareMethods.deleteAllSharedLinks(userId1);
expect(result.deletedCount).toBe(2);
// Verify shares for other users still exist
const remainingShares = await SharedLink.find({});
expect(remainingShares).toHaveLength(3);
expect(remainingShares.every((share) => share.user !== userId1)).toBe(true);
// Verify specific users' shares remain
const user2Shares = await SharedLink.find({ user: userId2 });
expect(user2Shares).toHaveLength(2);
const user3Shares = await SharedLink.find({ user: userId3 });
expect(user3Shares).toHaveLength(1);
});
});
describe('Edge Cases and Error Handling', () => {
test('should handle conversation with special characters in ID', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = 'conv|with|pipes';
await Conversation.create({
conversationId,
title: 'Special Conversation',
user: userId,
});
// Create a message so we can create a share
await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Test message',
isCreatedByUser: true,
});
const result = await shareMethods.createSharedLink(userId, conversationId);
expect(result).toBeDefined();
expect(result.conversationId).toBe(conversationId);
});
test('should handle messages with assistant_id', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const message = await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Assistant message',
isCreatedByUser: false,
model: 'asst_123456',
});
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: [message._id],
isPublic: true,
});
const result = await shareMethods.getSharedMessages(shareId);
expect(result?.messages[0].model).toMatch(/^a_/); // Should be anonymized
});
test('should handle concurrent operations', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationIds = Array.from({ length: 5 }, () => `conv_${nanoid()}`);
// Create conversations and messages
await Promise.all(
conversationIds.map(async (id) => {
await Conversation.create({
conversationId: id,
title: `Conversation ${id}`,
user: userId,
});
// Create a message for each conversation
await Message.create({
messageId: `msg_${nanoid()}`,
conversationId: id,
user: userId,
text: `Message for ${id}`,
isCreatedByUser: true,
});
}),
);
// Concurrent share creation
const createPromises = conversationIds.map((id) => shareMethods.createSharedLink(userId, id));
const results = await Promise.all(createPromises);
expect(results).toHaveLength(5);
results.forEach((result, index) => {
expect(result.shareId).toBeDefined();
expect(result.conversationId).toBe(conversationIds[index]);
});
});
test('should handle database errors gracefully', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
// Create conversation and message first
await Conversation.create({
conversationId,
title: 'Test Conversation',
user: userId,
});
await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Test message',
isCreatedByUser: true,
});
// Mock a database error
const originalCreate = SharedLink.create;
SharedLink.create = jest.fn().mockRejectedValue(new Error('Database error'));
await expect(shareMethods.createSharedLink(userId, conversationId)).rejects.toThrow(
'Error creating shared link',
);
SharedLink.create = originalCreate;
});
});
describe('Anonymization', () => {
beforeEach(() => {
// Ensure any mocks are restored before each test
jest.restoreAllMocks();
});
test('should consistently anonymize IDs', async () => {
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const messageId1 = `msg_${nanoid()}`;
const messageId2 = `msg_${nanoid()}`;
const messages = await Message.create([
{
messageId: messageId1,
conversationId,
user: userId,
text: 'First message',
isCreatedByUser: true,
parentMessageId: Constants.NO_PARENT,
},
{
messageId: messageId2,
conversationId,
user: userId,
text: 'Second message',
isCreatedByUser: false,
parentMessageId: messageId1, // Reference to first message
},
]);
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: messages.map((m) => m._id),
isPublic: true,
});
const result = await shareMethods.getSharedMessages(shareId);
// Check that anonymization is consistent within the same result
expect(result?.messages).toHaveLength(2);
// The second message's parentMessageId should match the first message's anonymized ID
expect(result?.messages[1].parentMessageId).toBe(result?.messages[0].messageId);
// Both messages should have the same anonymized conversationId
expect(result?.messages[0].conversationId).toBe(result?.conversationId);
expect(result?.messages[1].conversationId).toBe(result?.conversationId);
});
test('should handle NO_PARENT constant correctly', async () => {
const { Constants } = await import('librechat-data-provider');
const userId = new mongoose.Types.ObjectId().toString();
const conversationId = `conv_${nanoid()}`;
const shareId = `share_${nanoid()}`;
const message = await Message.create({
messageId: `msg_${nanoid()}`,
conversationId,
user: userId,
text: 'Root message',
isCreatedByUser: true,
parentMessageId: Constants.NO_PARENT,
});
await SharedLink.create({
shareId,
conversationId,
user: userId,
messages: [message._id],
isPublic: true,
});
const result = await shareMethods.getSharedMessages(shareId);
expect(result?.messages[0].parentMessageId).toBe(Constants.NO_PARENT);
});
});
});