- 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>
319 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|