cre-sync/__tests__/api/control-center/conversations.test.ts
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

319 lines
10 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { NextRequest } from 'next/server';
import { GET } from '@/app/api/v1/control-center/conversations/route';
import { prisma } from '@/lib/db';
import * as auth from '@/lib/auth';
// Get the mocked prisma instance
const mockPrisma = vi.mocked(prisma);
// Mock the auth module
vi.mock('@/lib/auth', () => ({
getSession: vi.fn()
}));
// Mock the conversationService through the control-center module
vi.mock('@/lib/control-center', () => ({
conversationService: {
listByUser: vi.fn()
}
}));
// Import the mocked conversation service
import { conversationService } from '@/lib/control-center';
const mockConversationService = vi.mocked(conversationService);
describe('Conversations API', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('GET /api/v1/control-center/conversations', () => {
it('should return 401 when not authenticated', async () => {
vi.mocked(auth.getSession).mockResolvedValue(null);
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(401);
const json = await response.json();
expect(json.error).toBe('Unauthorized');
});
it('should return conversations for authenticated user', async () => {
const mockUser = {
id: 'user-123',
email: 'test@example.com',
role: 'user' as const,
createdAt: new Date()
};
const mockConversations = [
{
id: 'conv-1',
userId: 'user-123',
title: 'First conversation',
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
_count: { messages: 5 }
},
{
id: 'conv-2',
userId: 'user-123',
title: 'Second conversation',
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-02'),
_count: { messages: 3 }
}
];
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue(mockConversations);
mockPrisma.controlCenterConversation.count.mockResolvedValue(2);
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.conversations).toHaveLength(2);
expect(json.total).toBe(2);
expect(json.limit).toBe(20);
expect(json.offset).toBe(0);
expect(mockConversationService.listByUser).toHaveBeenCalledWith('user-123', 20, 0);
});
it('should handle pagination parameters correctly', async () => {
const mockUser = {
id: 'user-456',
email: 'test@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(50);
const request = new NextRequest(
'http://localhost:3000/api/v1/control-center/conversations?limit=10&offset=20'
);
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.limit).toBe(10);
expect(json.offset).toBe(20);
expect(mockConversationService.listByUser).toHaveBeenCalledWith('user-456', 10, 20);
});
it('should use default pagination when parameters are invalid', async () => {
const mockUser = {
id: 'user-789',
email: 'test@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(0);
// Invalid parameters should parse as NaN, then parseInt will return NaN
const request = new NextRequest(
'http://localhost:3000/api/v1/control-center/conversations?limit=invalid&offset=bad'
);
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
// parseInt('invalid') returns NaN, which is falsy, but the code uses || not ??
// So NaN will use the default value
expect(mockConversationService.listByUser).toHaveBeenCalledWith(
'user-789',
expect.any(Number),
expect.any(Number)
);
});
it('should handle empty conversation list', async () => {
const mockUser = {
id: 'new-user',
email: 'new@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(0);
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.conversations).toEqual([]);
expect(json.total).toBe(0);
});
it('should return 500 on database error', async () => {
const mockUser = {
id: 'user-error',
email: 'error@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockRejectedValue(new Error('Database connection failed'));
// Suppress console.error for this test
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(500);
const json = await response.json();
expect(json.error).toBe('Failed to fetch conversations');
consoleSpy.mockRestore();
});
it('should count conversations correctly for the authenticated user only', async () => {
const mockUser = {
id: 'specific-user',
email: 'specific@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(25);
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.total).toBe(25);
expect(mockPrisma.controlCenterConversation.count).toHaveBeenCalledWith({
where: { userId: 'specific-user' }
});
});
it('should return conversations with message count', async () => {
const mockUser = {
id: 'user-with-convos',
email: 'convos@example.com',
role: 'user' as const,
createdAt: new Date()
};
const mockConversations = [
{
id: 'conv-with-messages',
userId: 'user-with-convos',
title: 'Conversation with messages',
createdAt: new Date(),
updatedAt: new Date(),
_count: { messages: 15 }
}
];
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue(mockConversations);
mockPrisma.controlCenterConversation.count.mockResolvedValue(1);
const request = new NextRequest('http://localhost:3000/api/v1/control-center/conversations');
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.conversations[0]._count.messages).toBe(15);
});
});
describe('Pagination edge cases', () => {
it('should handle large offset beyond total count', async () => {
const mockUser = {
id: 'user-large-offset',
email: 'large@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(5);
const request = new NextRequest(
'http://localhost:3000/api/v1/control-center/conversations?offset=100'
);
const response = await GET(request);
expect(response.status).toBe(200);
const json = await response.json();
expect(json.conversations).toEqual([]);
expect(json.total).toBe(5);
expect(json.offset).toBe(100);
});
it('should handle zero limit', async () => {
const mockUser = {
id: 'user-zero-limit',
email: 'zero@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(10);
const request = new NextRequest(
'http://localhost:3000/api/v1/control-center/conversations?limit=0'
);
const response = await GET(request);
expect(response.status).toBe(200);
// Note: limit=0 with parseInt returns 0, which is falsy but a valid number
// The API should handle this, returning 0 items
});
it('should handle negative pagination values gracefully', async () => {
const mockUser = {
id: 'user-negative',
email: 'negative@example.com',
role: 'user' as const,
createdAt: new Date()
};
vi.mocked(auth.getSession).mockResolvedValue({ user: mockUser });
mockConversationService.listByUser.mockResolvedValue([]);
mockPrisma.controlCenterConversation.count.mockResolvedValue(10);
const request = new NextRequest(
'http://localhost:3000/api/v1/control-center/conversations?limit=-5&offset=-10'
);
const response = await GET(request);
// The API should still return a response (behavior depends on Prisma handling)
expect(response.status).toBe(200);
});
});
});