mirror of https://github.com/buster-so/buster.git
Refactor chat permission checks and update database helper functions
Co-authored-by: dallin <dallin@buster.so>
This commit is contained in:
parent
79e7cb4b95
commit
964d107bb9
|
@ -4,30 +4,37 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
// Mock dependencies
|
||||
vi.mock('@trigger.dev/sdk/v3', () => ({
|
||||
tasks: {
|
||||
trigger: vi.fn(),
|
||||
},
|
||||
tasks: {
|
||||
trigger: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./services/chat-service', () => ({
|
||||
initializeChat: vi.fn(),
|
||||
initializeChat: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./services/chat-helpers', () => ({
|
||||
handleAssetChat: vi.fn(),
|
||||
handleAssetChat: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@buster/database', () => ({
|
||||
getUserOrganizationId: vi.fn(),
|
||||
checkChatPermission: vi.fn(),
|
||||
createMessage: vi.fn(),
|
||||
db: {
|
||||
transaction: vi.fn((callback: any) => callback({ insert: vi.fn() })),
|
||||
},
|
||||
getChatWithDetails: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
chats: {},
|
||||
messages: {},
|
||||
getUserOrganizationId: vi.fn(),
|
||||
createChat: vi.fn(),
|
||||
getChatWithDetails: vi.fn(),
|
||||
createMessage: vi.fn(),
|
||||
generateAssetMessages: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
db: {
|
||||
transaction: vi.fn().mockImplementation((callback: any) =>
|
||||
callback({
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([{ id: 'chat-123' }]),
|
||||
}),
|
||||
),
|
||||
},
|
||||
chats: {},
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
import { getUserOrganizationId } from '@buster/database';
|
||||
|
@ -37,168 +44,168 @@ import { handleAssetChat } from './services/chat-helpers';
|
|||
import { initializeChat } from './services/chat-service';
|
||||
|
||||
describe('createChatHandler', () => {
|
||||
const mockUser = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
avatarUrl: null,
|
||||
};
|
||||
const mockUser = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
const mockChat: ChatWithMessages = {
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
is_favorited: false,
|
||||
message_ids: ['msg-123'],
|
||||
messages: {
|
||||
'msg-123': {
|
||||
id: 'msg-123',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
request_message: {
|
||||
request: 'Hello',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'Test User',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
},
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_by: 'user-123',
|
||||
created_by_id: 'user-123',
|
||||
created_by_name: 'Test User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: false,
|
||||
};
|
||||
const mockChat: ChatWithMessages = {
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
is_favorited: false,
|
||||
message_ids: ['msg-123'],
|
||||
messages: {
|
||||
'msg-123': {
|
||||
id: 'msg-123',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
request_message: {
|
||||
request: 'Hello',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'Test User',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
},
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_by: 'user-123',
|
||||
created_by_id: 'user-123',
|
||||
created_by_name: 'Test User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(initializeChat).mockResolvedValue({
|
||||
chatId: 'chat-123',
|
||||
messageId: 'msg-123',
|
||||
chat: mockChat,
|
||||
});
|
||||
vi.mocked(getUserOrganizationId).mockResolvedValue({
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
role: 'admin',
|
||||
});
|
||||
vi.mocked(tasks.trigger).mockResolvedValue({ id: 'task-handle-123' } as any);
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(initializeChat).mockResolvedValue({
|
||||
chatId: 'chat-123',
|
||||
messageId: 'msg-123',
|
||||
chat: mockChat,
|
||||
});
|
||||
vi.mocked(getUserOrganizationId).mockResolvedValue({
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
role: 'admin',
|
||||
});
|
||||
vi.mocked(tasks.trigger).mockResolvedValue({ id: 'task-handle-123' } as any);
|
||||
});
|
||||
|
||||
it('should create a new chat with prompt', async () => {
|
||||
const result = await createChatHandler({ prompt: 'Hello' }, mockUser);
|
||||
it('should create a new chat with prompt', async () => {
|
||||
const result = await createChatHandler({ prompt: 'Hello' }, mockUser);
|
||||
|
||||
expect(initializeChat).toHaveBeenCalledWith(
|
||||
{ prompt: 'Hello' },
|
||||
mockUser,
|
||||
'550e8400-e29b-41d4-a716-446655440000'
|
||||
);
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(mockChat);
|
||||
});
|
||||
expect(initializeChat).toHaveBeenCalledWith(
|
||||
{ prompt: 'Hello' },
|
||||
mockUser,
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
);
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(mockChat);
|
||||
});
|
||||
|
||||
it('should handle asset-based chat creation', async () => {
|
||||
const assetChat = { ...mockChat, title: 'Asset Chat' };
|
||||
vi.mocked(handleAssetChat).mockResolvedValue(assetChat);
|
||||
it('should handle asset-based chat creation', async () => {
|
||||
const assetChat = { ...mockChat, title: 'Asset Chat' };
|
||||
vi.mocked(handleAssetChat).mockResolvedValue(assetChat);
|
||||
|
||||
const result = await createChatHandler(
|
||||
{ asset_id: 'asset-123', asset_type: 'metric_file' },
|
||||
mockUser
|
||||
);
|
||||
const result = await createChatHandler(
|
||||
{ asset_id: 'asset-123', asset_type: 'metric_file' },
|
||||
mockUser,
|
||||
);
|
||||
|
||||
expect(handleAssetChat).toHaveBeenCalledWith(
|
||||
'chat-123',
|
||||
'msg-123',
|
||||
'asset-123',
|
||||
'metric_file',
|
||||
mockUser,
|
||||
mockChat
|
||||
);
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(assetChat);
|
||||
});
|
||||
expect(handleAssetChat).toHaveBeenCalledWith(
|
||||
'chat-123',
|
||||
'msg-123',
|
||||
'asset-123',
|
||||
'metric_file',
|
||||
mockUser,
|
||||
mockChat,
|
||||
);
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(assetChat);
|
||||
});
|
||||
|
||||
it('should not trigger analyst task when no content', async () => {
|
||||
const result = await createChatHandler({}, mockUser).catch((e) => {
|
||||
expect(tasks.trigger).not.toHaveBeenCalled();
|
||||
expect(e).toBeInstanceOf(ChatError);
|
||||
});
|
||||
it('should not trigger analyst task when no content', async () => {
|
||||
const result = await createChatHandler({}, mockUser).catch((e) => {
|
||||
expect(tasks.trigger).not.toHaveBeenCalled();
|
||||
expect(e).toBeInstanceOf(ChatError);
|
||||
});
|
||||
|
||||
expect(tasks.trigger).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(tasks.trigger).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call handleAssetChat when prompt is provided with asset', async () => {
|
||||
const result = await createChatHandler(
|
||||
{ prompt: 'Hello', asset_id: 'asset-123', asset_type: 'metric_file' },
|
||||
mockUser
|
||||
);
|
||||
it('should not call handleAssetChat when prompt is provided with asset', async () => {
|
||||
const result = await createChatHandler(
|
||||
{ prompt: 'Hello', asset_id: 'asset-123', asset_type: 'metric_file' },
|
||||
mockUser,
|
||||
);
|
||||
|
||||
expect(handleAssetChat).not.toHaveBeenCalled();
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(mockChat);
|
||||
});
|
||||
expect(handleAssetChat).not.toHaveBeenCalled();
|
||||
expect(tasks.trigger).toHaveBeenCalledWith('analyst-agent-task', {
|
||||
message_id: 'msg-123',
|
||||
});
|
||||
expect(result).toEqual(mockChat);
|
||||
});
|
||||
|
||||
it('should handle trigger errors gracefully', async () => {
|
||||
vi.mocked(tasks.trigger).mockReset().mockRejectedValue(new Error('Trigger failed'));
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
it('should handle trigger errors gracefully', async () => {
|
||||
vi.mocked(tasks.trigger).mockReset().mockRejectedValue(new Error('Trigger failed'));
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.INTERNAL_ERROR,
|
||||
message: 'An unexpected error occurred while creating the chat',
|
||||
details: { originalError: 'Trigger failed' },
|
||||
});
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.INTERNAL_ERROR,
|
||||
message: 'An unexpected error occurred while creating the chat',
|
||||
details: { originalError: 'Trigger failed' },
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Chat creation failed:', expect.any(Object));
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Chat creation failed:', expect.any(Object));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw MISSING_ORGANIZATION error when user has no org', async () => {
|
||||
const userWithoutOrg = {
|
||||
...mockUser,
|
||||
user_metadata: {},
|
||||
};
|
||||
vi.mocked(getUserOrganizationId).mockResolvedValue(null);
|
||||
it('should throw MISSING_ORGANIZATION error when user has no org', async () => {
|
||||
const userWithoutOrg = {
|
||||
...mockUser,
|
||||
user_metadata: {},
|
||||
};
|
||||
vi.mocked(getUserOrganizationId).mockResolvedValue(null);
|
||||
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, userWithoutOrg)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.MISSING_ORGANIZATION,
|
||||
message: 'User is not associated with an organization',
|
||||
});
|
||||
});
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, userWithoutOrg)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.MISSING_ORGANIZATION,
|
||||
message: 'User is not associated with an organization',
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-throw ChatError instances', async () => {
|
||||
const chatError = new ChatError(ChatErrorCode.PERMISSION_DENIED, 'No permission', 403);
|
||||
vi.mocked(initializeChat).mockRejectedValue(chatError);
|
||||
it('should re-throw ChatError instances', async () => {
|
||||
const chatError = new ChatError(ChatErrorCode.PERMISSION_DENIED, 'No permission', 403);
|
||||
vi.mocked(initializeChat).mockRejectedValue(chatError);
|
||||
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.PERMISSION_DENIED,
|
||||
message: 'No permission',
|
||||
details: undefined,
|
||||
});
|
||||
});
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.PERMISSION_DENIED,
|
||||
message: 'No permission',
|
||||
details: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap unexpected errors', async () => {
|
||||
vi.mocked(initializeChat).mockRejectedValue(new Error('Database error'));
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
it('should wrap unexpected errors', async () => {
|
||||
vi.mocked(initializeChat).mockRejectedValue(new Error('Database error'));
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.INTERNAL_ERROR,
|
||||
message: 'An unexpected error occurred while creating the chat',
|
||||
details: { originalError: 'Database error' },
|
||||
});
|
||||
await expect(createChatHandler({ prompt: 'Hello' }, mockUser)).rejects.toMatchObject({
|
||||
code: ChatErrorCode.INTERNAL_ERROR,
|
||||
message: 'An unexpected error occurred while creating the chat',
|
||||
details: { originalError: 'Database error' },
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,288 +2,293 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
// Mock the database connection and database functions BEFORE any other imports
|
||||
vi.mock('@buster/database/connection', () => ({
|
||||
initializePool: vi.fn(),
|
||||
getPool: vi.fn(),
|
||||
initializePool: vi.fn(),
|
||||
getPool: vi.fn(),
|
||||
}));
|
||||
vi.mock('@buster/database', () => ({
|
||||
checkChatPermission: vi.fn(),
|
||||
createMessage: vi.fn(),
|
||||
db: {
|
||||
transaction: vi.fn((callback: any) => callback({ insert: vi.fn() })),
|
||||
},
|
||||
getChatWithDetails: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
chats: {},
|
||||
messages: {},
|
||||
createMessage: vi.fn(),
|
||||
db: {
|
||||
transaction: vi.fn((callback: any) => callback({ insert: vi.fn() })),
|
||||
},
|
||||
getChatWithDetails: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
chats: {},
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
// Mock the access-controls package
|
||||
vi.mock('@buster/access-controls', () => ({
|
||||
canUserAccessChatCached: vi.fn(),
|
||||
}));
|
||||
|
||||
import { canUserAccessChatCached } from '@buster/access-controls';
|
||||
import * as database from '@buster/database';
|
||||
import type { Chat, Message } from '@buster/database';
|
||||
import { ChatError, ChatErrorCode } from '@buster/server-shared/chats';
|
||||
import { buildChatWithMessages, handleExistingChat, handleNewChat } from './chat-helpers';
|
||||
|
||||
const mockUser = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
avatarUrl: null,
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
const mockChat: Chat = {
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
updatedBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
mostRecentVersionNumber: null,
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
updatedBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
mostRecentVersionNumber: null,
|
||||
};
|
||||
|
||||
const mockMessage: Message = {
|
||||
id: 'msg-123',
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
finalReasoningMessage: null,
|
||||
isCompleted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
feedback: null,
|
||||
id: 'msg-123',
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
finalReasoningMessage: null,
|
||||
isCompleted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
feedback: null,
|
||||
};
|
||||
|
||||
describe('buildChatWithMessages', () => {
|
||||
it('should build a ChatWithMessages object from database entities', () => {
|
||||
const result = buildChatWithMessages(mockChat, [mockMessage], mockUser, true);
|
||||
expect(result).toMatchObject({
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
is_favorited: true,
|
||||
message_ids: ['msg-123'],
|
||||
messages: {
|
||||
'msg-123': {
|
||||
id: 'msg-123',
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
request_message: {
|
||||
request: 'Test message',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'Test User',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
},
|
||||
},
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
created_by: '550e8400-e29b-41d4-a716-446655440001',
|
||||
created_by_id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
created_by_name: 'Test User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: false,
|
||||
permission: 'owner',
|
||||
});
|
||||
});
|
||||
it('should build a ChatWithMessages object from database entities', () => {
|
||||
const result = buildChatWithMessages(mockChat, [mockMessage], mockUser, true);
|
||||
expect(result).toMatchObject({
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
is_favorited: true,
|
||||
message_ids: ['msg-123'],
|
||||
messages: {
|
||||
'msg-123': {
|
||||
id: 'msg-123',
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
request_message: {
|
||||
request: 'Test message',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'Test User',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
},
|
||||
},
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
created_by: '550e8400-e29b-41d4-a716-446655440001',
|
||||
created_by_id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
created_by_name: 'Test User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: false,
|
||||
permission: 'owner',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing creator details', () => {
|
||||
const result = buildChatWithMessages(mockChat, [], null, false);
|
||||
it('should handle missing creator details', () => {
|
||||
const result = buildChatWithMessages(mockChat, [], null, false);
|
||||
|
||||
expect(result.created_by_name).toBe('Unknown User');
|
||||
expect(result.created_by_avatar).toBeNull();
|
||||
});
|
||||
expect(result.created_by_name).toBe('Unknown User');
|
||||
expect(result.created_by_avatar).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleExistingChat', () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
};
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle existing chat with new message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
it('should handle existing chat with new message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
|
||||
const mockMessage = {
|
||||
id: 'message-1',
|
||||
chatId: 'chat-1',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
isCompleted: false,
|
||||
deletedAt: null,
|
||||
finalReasoningMessage: null,
|
||||
feedback: null,
|
||||
} as Message;
|
||||
const mockMessage = {
|
||||
id: 'message-1',
|
||||
chatId: 'chat-1',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
isCompleted: false,
|
||||
deletedAt: null,
|
||||
finalReasoningMessage: null,
|
||||
feedback: null,
|
||||
} as Message;
|
||||
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue({
|
||||
chat: mockChat,
|
||||
user: mockUser as unknown as any,
|
||||
isFavorited: false,
|
||||
});
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue({
|
||||
chat: mockChat,
|
||||
user: mockUser as unknown as any,
|
||||
isFavorited: false,
|
||||
});
|
||||
|
||||
vi.mocked(database.checkChatPermission).mockResolvedValue(true);
|
||||
vi.mocked(database.createMessage).mockResolvedValue(mockMessage);
|
||||
vi.mocked(database.getMessagesForChat).mockResolvedValue([mockMessage]);
|
||||
vi.mocked(canUserAccessChatCached).mockResolvedValue(true);
|
||||
vi.mocked(database.createMessage).mockResolvedValue(mockMessage);
|
||||
vi.mocked(database.getMessagesForChat).mockResolvedValue([mockMessage]);
|
||||
|
||||
const result = await handleExistingChat('chat-1', 'message-1', 'Test message', mockUser);
|
||||
const result = await handleExistingChat('chat-1', 'message-1', 'Test message', mockUser);
|
||||
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
expect(result.chat.messages['message-1']).toBeDefined();
|
||||
});
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
expect(result.chat.messages['message-1']).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw error if chat not found', async () => {
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue(null);
|
||||
it('should throw error if chat not found', async () => {
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
handleExistingChat('chat-1', 'message-1', 'Test message', mockUser)
|
||||
).rejects.toThrow(new ChatError(ChatErrorCode.CHAT_NOT_FOUND, 'Chat not found', 404));
|
||||
});
|
||||
await expect(
|
||||
handleExistingChat('chat-1', 'message-1', 'Test message', mockUser),
|
||||
).rejects.toThrow(new ChatError(ChatErrorCode.CHAT_NOT_FOUND, 'Chat not found', 404));
|
||||
});
|
||||
|
||||
it('should throw error if permission denied', async () => {
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue({
|
||||
chat: { id: 'chat-1' } as Chat,
|
||||
user: null,
|
||||
isFavorited: false,
|
||||
});
|
||||
vi.mocked(database.checkChatPermission).mockResolvedValue(false);
|
||||
it('should throw error if permission denied', async () => {
|
||||
vi.mocked(database.getChatWithDetails).mockResolvedValue({
|
||||
chat: { id: 'chat-1' } as Chat,
|
||||
user: null,
|
||||
isFavorited: false,
|
||||
});
|
||||
vi.mocked(canUserAccessChatCached).mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
handleExistingChat('chat-1', 'message-1', 'Test message', mockUser)
|
||||
).rejects.toThrow(
|
||||
new ChatError(
|
||||
ChatErrorCode.PERMISSION_DENIED,
|
||||
'You do not have permission to access this chat',
|
||||
403
|
||||
)
|
||||
);
|
||||
});
|
||||
await expect(
|
||||
handleExistingChat('chat-1', 'message-1', 'Test message', mockUser),
|
||||
).rejects.toThrow(
|
||||
new ChatError(
|
||||
ChatErrorCode.PERMISSION_DENIED,
|
||||
'You do not have permission to access this chat',
|
||||
403,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNewChat', () => {
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
};
|
||||
const mockUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create new chat with message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
it('should create new chat with message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
|
||||
const mockTx = {
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
};
|
||||
const mockTx = {
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
};
|
||||
|
||||
vi.mocked(database.db.transaction).mockImplementation((callback: any) => callback(mockTx));
|
||||
vi.mocked(database.db.transaction).mockImplementation((callback: any) => callback(mockTx));
|
||||
|
||||
const result = await handleNewChat({
|
||||
title: 'Test Chat',
|
||||
messageId: 'message-1',
|
||||
prompt: 'Test message',
|
||||
user: mockUser,
|
||||
organizationId: 'org-1',
|
||||
});
|
||||
const result = await handleNewChat({
|
||||
title: 'Test Chat',
|
||||
messageId: 'message-1',
|
||||
prompt: 'Test message',
|
||||
user: mockUser,
|
||||
organizationId: 'org-1',
|
||||
});
|
||||
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
});
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
});
|
||||
|
||||
it('should create new chat without message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
it('should create new chat without message', async () => {
|
||||
const mockChat = {
|
||||
id: 'chat-1',
|
||||
title: 'Test Chat',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdBy: 'user-1',
|
||||
updatedBy: 'user-1',
|
||||
organizationId: 'org-1',
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
} as Chat;
|
||||
|
||||
const mockTx = {
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
};
|
||||
const mockTx = {
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
};
|
||||
|
||||
vi.mocked(database.db.transaction).mockImplementation((callback: any) => callback(mockTx));
|
||||
vi.mocked(database.db.transaction).mockImplementation((callback: any) => callback(mockTx));
|
||||
|
||||
const result = await handleNewChat({
|
||||
title: 'Test Chat',
|
||||
messageId: 'message-1',
|
||||
prompt: undefined,
|
||||
user: mockUser,
|
||||
organizationId: 'org-1',
|
||||
});
|
||||
const result = await handleNewChat({
|
||||
title: 'Test Chat',
|
||||
messageId: 'message-1',
|
||||
prompt: undefined,
|
||||
user: mockUser,
|
||||
organizationId: 'org-1',
|
||||
});
|
||||
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
expect(Object.keys(result.chat.messages)).toHaveLength(0);
|
||||
});
|
||||
expect(result.chatId).toBe('chat-1');
|
||||
expect(result.messageId).toBe('message-1');
|
||||
expect(Object.keys(result.chat.messages)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,148 +5,164 @@ import { initializeChat } from './chat-service';
|
|||
|
||||
// Import mocked functions
|
||||
import {
|
||||
checkChatPermission,
|
||||
createChat,
|
||||
createMessage,
|
||||
generateAssetMessages,
|
||||
getChatWithDetails,
|
||||
getMessagesForChat,
|
||||
createChat,
|
||||
createMessage,
|
||||
generateAssetMessages,
|
||||
getChatWithDetails,
|
||||
getMessagesForChat,
|
||||
} from '@buster/database';
|
||||
|
||||
const mockUser = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
id: '550e8400-e29b-41d4-a716-446655440001',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
const mockChat: Chat = {
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
updatedBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
mostRecentVersionNumber: null,
|
||||
id: 'chat-123',
|
||||
title: 'Test Chat',
|
||||
organizationId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
createdBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
updatedBy: '550e8400-e29b-41d4-a716-446655440001',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
publiclyAccessible: false,
|
||||
deletedAt: null,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
mostRecentFileId: null,
|
||||
mostRecentFileType: null,
|
||||
mostRecentVersionNumber: null,
|
||||
};
|
||||
|
||||
const mockMessage: Message = {
|
||||
id: 'msg-123',
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
finalReasoningMessage: null,
|
||||
isCompleted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
feedback: null,
|
||||
id: 'msg-123',
|
||||
chatId: 'chat-123',
|
||||
createdBy: 'user-123',
|
||||
requestMessage: 'Test message',
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: 'Test message',
|
||||
rawLlmMessages: {},
|
||||
finalReasoningMessage: null,
|
||||
isCompleted: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
feedback: null,
|
||||
};
|
||||
|
||||
// Mock database functions
|
||||
vi.mock('@buster/database', () => ({
|
||||
db: {
|
||||
transaction: vi.fn().mockImplementation(async (callback) => {
|
||||
return callback({
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
});
|
||||
}),
|
||||
},
|
||||
chats: {},
|
||||
messages: {},
|
||||
createChat: vi.fn(),
|
||||
getChatWithDetails: vi.fn(),
|
||||
createMessage: vi.fn(),
|
||||
checkChatPermission: vi.fn(),
|
||||
generateAssetMessages: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
db: {
|
||||
transaction: vi.fn().mockImplementation(async (callback) => {
|
||||
return callback({
|
||||
insert: vi.fn().mockReturnThis(),
|
||||
values: vi.fn().mockReturnThis(),
|
||||
returning: vi.fn().mockResolvedValue([mockChat]),
|
||||
});
|
||||
}),
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn().mockResolvedValue([{ chatId: 'chat-123' }]),
|
||||
},
|
||||
chats: {},
|
||||
messages: {},
|
||||
createChat: vi.fn(),
|
||||
getChatWithDetails: vi.fn(),
|
||||
createMessage: vi.fn(),
|
||||
generateAssetMessages: vi.fn(),
|
||||
getMessagesForChat: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock access-controls
|
||||
vi.mock('@buster/access-controls', () => ({
|
||||
canUserAccessChatCached: vi.fn(),
|
||||
}));
|
||||
|
||||
import { canUserAccessChatCached } from '@buster/access-controls';
|
||||
|
||||
describe('chat-service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initializeChat', () => {
|
||||
it('should create a new chat when chat_id is not provided', async () => {
|
||||
const result = await initializeChat(
|
||||
{ prompt: 'Hello' },
|
||||
mockUser,
|
||||
'550e8400-e29b-41d4-a716-446655440000'
|
||||
);
|
||||
describe('initializeChat', () => {
|
||||
it('should create a new chat when chat_id is not provided', async () => {
|
||||
const result = await initializeChat(
|
||||
{ prompt: 'Hello' },
|
||||
mockUser,
|
||||
'550e8400-e29b-41d4-a716-446655440000',
|
||||
);
|
||||
|
||||
expect(result.chatId).toBe('chat-123');
|
||||
expect(result.chat.title).toBe('Test Chat');
|
||||
});
|
||||
expect(result.chatId).toBe('chat-123');
|
||||
expect(result.chat.title).toBe('Test Chat');
|
||||
});
|
||||
|
||||
it('should add message to existing chat when chat_id is provided', async () => {
|
||||
vi.mocked(checkChatPermission).mockResolvedValue(true);
|
||||
vi.mocked(getChatWithDetails).mockResolvedValue({
|
||||
chat: mockChat,
|
||||
user: { id: 'user-123', name: 'Test User', avatarUrl: null } as any,
|
||||
isFavorited: false,
|
||||
});
|
||||
vi.mocked(getMessagesForChat).mockResolvedValue([mockMessage]);
|
||||
vi.mocked(createMessage).mockResolvedValue({
|
||||
...mockMessage,
|
||||
id: 'msg-456',
|
||||
requestMessage: 'Follow up',
|
||||
});
|
||||
it('should add message to existing chat when chat_id is provided', async () => {
|
||||
vi.mocked(canUserAccessChatCached).mockResolvedValue(true);
|
||||
vi.mocked(getChatWithDetails).mockResolvedValue({
|
||||
chat: mockChat,
|
||||
user: { id: 'user-123', name: 'Test User', avatarUrl: null } as any,
|
||||
isFavorited: false,
|
||||
});
|
||||
vi.mocked(getMessagesForChat).mockResolvedValue([mockMessage]);
|
||||
vi.mocked(createMessage).mockResolvedValue({
|
||||
...mockMessage,
|
||||
id: 'msg-456',
|
||||
requestMessage: 'Follow up',
|
||||
});
|
||||
|
||||
const result = await initializeChat(
|
||||
{ chat_id: 'chat-123', prompt: 'Follow up' },
|
||||
mockUser,
|
||||
'org-123'
|
||||
);
|
||||
const result = await initializeChat(
|
||||
{ chat_id: 'chat-123', prompt: 'Follow up' },
|
||||
mockUser,
|
||||
'org-123',
|
||||
);
|
||||
|
||||
expect(checkChatPermission).toHaveBeenCalledWith('chat-123', mockUser.id);
|
||||
expect(createMessage).toHaveBeenCalledWith({
|
||||
chatId: 'chat-123',
|
||||
content: 'Follow up',
|
||||
userId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
messageId: expect.any(String),
|
||||
});
|
||||
expect(result.chatId).toBe('chat-123');
|
||||
});
|
||||
expect(canUserAccessChatCached).toHaveBeenCalledWith({
|
||||
userId: mockUser.id,
|
||||
chatId: 'chat-123',
|
||||
});
|
||||
expect(createMessage).toHaveBeenCalledWith({
|
||||
chatId: 'chat-123',
|
||||
content: 'Follow up',
|
||||
userId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
messageId: expect.any(String),
|
||||
});
|
||||
expect(result.chatId).toBe('chat-123');
|
||||
});
|
||||
|
||||
it('should throw PERMISSION_DENIED error when user lacks permission', async () => {
|
||||
vi.mocked(checkChatPermission).mockResolvedValue(false);
|
||||
it('should throw PERMISSION_DENIED error when user lacks permission', async () => {
|
||||
vi.mocked(canUserAccessChatCached).mockResolvedValue(false);
|
||||
vi.mocked(getChatWithDetails).mockResolvedValue({
|
||||
chat: mockChat,
|
||||
user: { id: 'user-123', name: 'Test User', avatarUrl: null } as any,
|
||||
isFavorited: false,
|
||||
});
|
||||
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123')
|
||||
).rejects.toThrow(ChatError);
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123'),
|
||||
).rejects.toThrow(ChatError);
|
||||
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123')
|
||||
).rejects.toMatchObject({
|
||||
code: ChatErrorCode.PERMISSION_DENIED,
|
||||
statusCode: 403,
|
||||
});
|
||||
});
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123'),
|
||||
).rejects.toMatchObject({
|
||||
code: ChatErrorCode.PERMISSION_DENIED,
|
||||
statusCode: 403,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw CHAT_NOT_FOUND error when chat does not exist', async () => {
|
||||
vi.mocked(checkChatPermission).mockResolvedValue(true);
|
||||
vi.mocked(getChatWithDetails).mockResolvedValue(null);
|
||||
it('should throw CHAT_NOT_FOUND error when chat does not exist', async () => {
|
||||
vi.mocked(getChatWithDetails).mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123')
|
||||
).rejects.toMatchObject({
|
||||
code: ChatErrorCode.CHAT_NOT_FOUND,
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
await expect(
|
||||
initializeChat({ chat_id: 'chat-123', prompt: 'Hello' }, mockUser, 'org-123'),
|
||||
).rejects.toMatchObject({
|
||||
code: ChatErrorCode.CHAT_NOT_FOUND,
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,28 +11,28 @@ export type User = InferSelectModel<typeof users>;
|
|||
|
||||
// Create a type for updateable chat fields by excluding auto-managed fields
|
||||
type UpdateableChatFields = Partial<
|
||||
Omit<typeof chats.$inferInsert, 'id' | 'createdAt' | 'deletedAt'>
|
||||
Omit<typeof chats.$inferInsert, 'id' | 'createdAt' | 'deletedAt'>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Input/output schemas for type safety
|
||||
*/
|
||||
export const CreateChatInputSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
userId: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
title: z.string().min(1),
|
||||
userId: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const GetChatInputSchema = z.object({
|
||||
chatId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
chatId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const CreateMessageInputSchema = z.object({
|
||||
chatId: z.string().uuid(),
|
||||
content: z.string(),
|
||||
userId: z.string().uuid(),
|
||||
messageId: z.string().uuid().optional(),
|
||||
chatId: z.string().uuid(),
|
||||
content: z.string(),
|
||||
userId: z.string().uuid(),
|
||||
messageId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export type CreateChatInput = z.infer<typeof CreateChatInputSchema>;
|
||||
|
@ -43,155 +43,136 @@ export type CreateMessageInput = z.infer<typeof CreateMessageInputSchema>;
|
|||
* Create a new chat
|
||||
*/
|
||||
export async function createChat(input: CreateChatInput): Promise<Chat> {
|
||||
try {
|
||||
const validated = CreateChatInputSchema.parse(input);
|
||||
try {
|
||||
const validated = CreateChatInputSchema.parse(input);
|
||||
|
||||
const [chat] = await db
|
||||
.insert(chats)
|
||||
.values({
|
||||
title: validated.title,
|
||||
organizationId: validated.organizationId,
|
||||
createdBy: validated.userId,
|
||||
updatedBy: validated.userId,
|
||||
publiclyAccessible: false,
|
||||
})
|
||||
.returning();
|
||||
const [chat] = await db
|
||||
.insert(chats)
|
||||
.values({
|
||||
title: validated.title,
|
||||
organizationId: validated.organizationId,
|
||||
createdBy: validated.userId,
|
||||
updatedBy: validated.userId,
|
||||
publiclyAccessible: false,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!chat) {
|
||||
throw new Error('Failed to create chat');
|
||||
}
|
||||
if (!chat) {
|
||||
throw new Error('Failed to create chat');
|
||||
}
|
||||
|
||||
return chat;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid chat input: ${error.errors.map((e) => e.message).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return chat;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid chat input: ${error.errors.map((e) => e.message).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chat with user and favorite information
|
||||
*/
|
||||
export async function getChatWithDetails(input: GetChatInput): Promise<{
|
||||
chat: Chat;
|
||||
user: User | null;
|
||||
isFavorited: boolean;
|
||||
chat: Chat;
|
||||
user: User | null;
|
||||
isFavorited: boolean;
|
||||
} | null> {
|
||||
const validated = GetChatInputSchema.parse(input);
|
||||
const validated = GetChatInputSchema.parse(input);
|
||||
|
||||
// Get chat with creator info
|
||||
const result = await db
|
||||
.select({
|
||||
chat: chats,
|
||||
user: users,
|
||||
})
|
||||
.from(chats)
|
||||
.leftJoin(users, eq(chats.createdBy, users.id))
|
||||
.where(and(eq(chats.id, validated.chatId), isNull(chats.deletedAt)))
|
||||
.limit(1);
|
||||
// Get chat with creator info
|
||||
const result = await db
|
||||
.select({
|
||||
chat: chats,
|
||||
user: users,
|
||||
})
|
||||
.from(chats)
|
||||
.leftJoin(users, eq(chats.createdBy, users.id))
|
||||
.where(and(eq(chats.id, validated.chatId), isNull(chats.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (!result.length || !result[0]?.chat) {
|
||||
return null;
|
||||
}
|
||||
if (!result.length || !result[0]?.chat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if favorited
|
||||
const favorite = await db
|
||||
.select()
|
||||
.from(userFavorites)
|
||||
.where(
|
||||
and(
|
||||
eq(userFavorites.userId, validated.userId),
|
||||
eq(userFavorites.assetId, validated.chatId),
|
||||
eq(userFavorites.assetType, 'chat'),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
// Check if favorited
|
||||
const favorite = await db
|
||||
.select()
|
||||
.from(userFavorites)
|
||||
.where(
|
||||
and(
|
||||
eq(userFavorites.userId, validated.userId),
|
||||
eq(userFavorites.assetId, validated.chatId),
|
||||
eq(userFavorites.assetType, 'chat')
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const { chat, user } = result[0];
|
||||
return {
|
||||
chat,
|
||||
user,
|
||||
isFavorited: favorite.length > 0,
|
||||
};
|
||||
const { chat, user } = result[0];
|
||||
return {
|
||||
chat,
|
||||
user,
|
||||
isFavorited: favorite.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new message in a chat
|
||||
*/
|
||||
export async function createMessage(input: CreateMessageInput): Promise<Message> {
|
||||
try {
|
||||
const validated = CreateMessageInputSchema.parse(input);
|
||||
try {
|
||||
const validated = CreateMessageInputSchema.parse(input);
|
||||
|
||||
const messageId = validated.messageId || crypto.randomUUID();
|
||||
const messageId = validated.messageId || crypto.randomUUID();
|
||||
|
||||
// Use transaction to ensure atomicity
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const [message] = await tx
|
||||
.insert(messages)
|
||||
.values({
|
||||
id: messageId,
|
||||
chatId: validated.chatId,
|
||||
createdBy: validated.userId,
|
||||
requestMessage: validated.content,
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: validated.content.substring(0, 255), // Ensure title fits in database
|
||||
rawLlmMessages: {},
|
||||
isCompleted: false,
|
||||
})
|
||||
.returning();
|
||||
// Use transaction to ensure atomicity
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const [message] = await tx
|
||||
.insert(messages)
|
||||
.values({
|
||||
id: messageId,
|
||||
chatId: validated.chatId,
|
||||
createdBy: validated.userId,
|
||||
requestMessage: validated.content,
|
||||
responseMessages: {},
|
||||
reasoning: {},
|
||||
title: validated.content.substring(0, 255), // Ensure title fits in database
|
||||
rawLlmMessages: {},
|
||||
isCompleted: false,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!message) {
|
||||
throw new Error('Failed to create message');
|
||||
}
|
||||
if (!message) {
|
||||
throw new Error('Failed to create message');
|
||||
}
|
||||
|
||||
// Update chat's updated_at timestamp
|
||||
await tx
|
||||
.update(chats)
|
||||
.set({ updatedAt: new Date().toISOString() })
|
||||
.where(eq(chats.id, validated.chatId));
|
||||
// Update chat's updated_at timestamp
|
||||
await tx
|
||||
.update(chats)
|
||||
.set({ updatedAt: new Date().toISOString() })
|
||||
.where(eq(chats.id, validated.chatId));
|
||||
|
||||
return message;
|
||||
});
|
||||
return message;
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid message input: ${error.errors.map((e) => e.message).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has permission to access a chat
|
||||
*/
|
||||
export async function checkChatPermission(chatId: string, userId: string): Promise<boolean> {
|
||||
const chat = await db
|
||||
.select()
|
||||
.from(chats)
|
||||
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (!chat.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For now, only check if user is the creator
|
||||
// TODO: Add more sophisticated permission checking with asset_permissions table
|
||||
return chat[0]?.createdBy === userId;
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid message input: ${error.errors.map((e) => e.message).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages for a chat
|
||||
*/
|
||||
export async function getMessagesForChat(chatId: string): Promise<Message[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(and(eq(messages.chatId, chatId), isNull(messages.deletedAt)))
|
||||
.orderBy(messages.createdAt);
|
||||
return db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(and(eq(messages.chatId, chatId), isNull(messages.deletedAt)))
|
||||
.orderBy(messages.createdAt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,50 +184,50 @@ export async function getMessagesForChat(chatId: string): Promise<Message[]> {
|
|||
* @returns Success status
|
||||
*/
|
||||
export async function updateChat(
|
||||
chatId: string,
|
||||
fields: UpdateableChatFields,
|
||||
chatId: string,
|
||||
fields: UpdateableChatFields
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
// First verify the chat exists and is not deleted
|
||||
const existingChat = await db
|
||||
.select({ id: chats.id })
|
||||
.from(chats)
|
||||
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)))
|
||||
.limit(1);
|
||||
try {
|
||||
// First verify the chat exists and is not deleted
|
||||
const existingChat = await db
|
||||
.select({ id: chats.id })
|
||||
.from(chats)
|
||||
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (existingChat.length === 0) {
|
||||
throw new Error(`Chat not found or has been deleted: ${chatId}`);
|
||||
}
|
||||
if (existingChat.length === 0) {
|
||||
throw new Error(`Chat not found or has been deleted: ${chatId}`);
|
||||
}
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updateData: Partial<UpdateableChatFields> & { updatedAt: string } = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
// Build update object with only provided fields
|
||||
const updateData: Partial<UpdateableChatFields> & { updatedAt: string } = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Only add fields that are actually provided (not undefined)
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt') {
|
||||
updateData[key] = value;
|
||||
}
|
||||
}
|
||||
// Only add fields that are actually provided (not undefined)
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt') {
|
||||
updateData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// If updatedAt was explicitly provided, use that instead
|
||||
if ('updatedAt' in fields && fields.updatedAt !== undefined) {
|
||||
updateData.updatedAt = fields.updatedAt;
|
||||
}
|
||||
// If updatedAt was explicitly provided, use that instead
|
||||
if ('updatedAt' in fields && fields.updatedAt !== undefined) {
|
||||
updateData.updatedAt = fields.updatedAt;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(chats)
|
||||
.set(updateData)
|
||||
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)));
|
||||
await db
|
||||
.update(chats)
|
||||
.set(updateData)
|
||||
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update chat fields:', error);
|
||||
// Re-throw our specific validation errors
|
||||
if (error instanceof Error && error.message.includes('Chat not found')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to update chat fields for chat ${chatId}`);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update chat fields:', error);
|
||||
// Re-throw our specific validation errors
|
||||
if (error instanceof Error && error.message.includes('Chat not found')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to update chat fields for chat ${chatId}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ export {
|
|||
updateChat,
|
||||
getChatWithDetails,
|
||||
createMessage,
|
||||
checkChatPermission,
|
||||
getMessagesForChat,
|
||||
CreateChatInputSchema,
|
||||
GetChatInputSchema,
|
||||
|
|
Loading…
Reference in New Issue