Files
sim/apps/sim/app/api/chat/edit/[id]/route.test.ts
Waleed Latif a7c8f5dfe9 fix(chat-deploy): fixed permissions to match the workspace permissions, admins can deploy & edit & delete (#753)
* fix(chat-deploy): fixed permissions to match the workspace permissions, admins can deploy & edit & delete

* fixed hanging chat deploy modal

* remove unnecessary fallback
2025-07-22 18:21:33 -07:00

397 lines
13 KiB
TypeScript

import { NextRequest } from 'next/server'
/**
* Tests for chat edit API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('Chat Edit API Route', () => {
const mockSelect = vi.fn()
const mockFrom = vi.fn()
const mockWhere = vi.fn()
const mockLimit = vi.fn()
const mockUpdate = vi.fn()
const mockSet = vi.fn()
const mockDelete = vi.fn()
const mockCreateSuccessResponse = vi.fn()
const mockCreateErrorResponse = vi.fn()
const mockEncryptSecret = vi.fn()
const mockCheckChatAccess = vi.fn()
beforeEach(() => {
vi.resetModules()
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
mockWhere.mockReturnValue({ limit: mockLimit })
mockUpdate.mockReturnValue({ set: mockSet })
mockSet.mockReturnValue({ where: mockWhere })
mockDelete.mockReturnValue({ where: mockWhere })
vi.doMock('@/db', () => ({
db: {
select: mockSelect,
update: mockUpdate,
delete: mockDelete,
},
}))
vi.doMock('@/db/schema', () => ({
chat: { id: 'id', subdomain: 'subdomain', userId: 'userId' },
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}))
vi.doMock('@/app/api/workflows/utils', () => ({
createSuccessResponse: mockCreateSuccessResponse.mockImplementation((data) => {
return new Response(JSON.stringify(data), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}),
createErrorResponse: mockCreateErrorResponse.mockImplementation((message, status = 500) => {
return new Response(JSON.stringify({ error: message }), {
status,
headers: { 'Content-Type': 'application/json' },
})
}),
}))
vi.doMock('@/lib/utils', () => ({
encryptSecret: mockEncryptSecret.mockResolvedValue({ encrypted: 'encrypted-password' }),
}))
vi.doMock('@/lib/urls/utils', () => ({
getBaseDomain: vi.fn().mockReturnValue('localhost:3000'),
}))
vi.doMock('@/lib/environment', () => ({
isDev: true,
}))
vi.doMock('@/app/api/chat/utils', () => ({
checkChatAccess: mockCheckChatAccess,
}))
})
afterEach(() => {
vi.clearAllMocks()
})
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
const { GET } = await import('./route')
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
})
it('should return 404 when chat not found or access denied', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
const { GET } = await import('./route')
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})
it('should return chat details when user has access', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
const mockChat = {
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
description: 'A test chat',
password: 'encrypted-password',
customizations: { primaryColor: '#000000' },
}
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123')
const { GET } = await import('./route')
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(200)
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
description: 'A test chat',
customizations: { primaryColor: '#000000' },
chatUrl: 'http://test-chat.localhost:3000',
hasPassword: true,
})
})
})
describe('PATCH', () => {
it('should return 401 when user is not authenticated', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ title: 'Updated Chat' }),
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
})
it('should return 404 when chat not found or access denied', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ title: 'Updated Chat' }),
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})
it('should update chat when user has access', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
const mockChat = {
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
authType: 'public',
}
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ title: 'Updated Chat', description: 'Updated description' }),
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(200)
expect(mockUpdate).toHaveBeenCalled()
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
id: 'chat-123',
chatUrl: 'http://test-chat.localhost:3000',
message: 'Chat deployment updated successfully',
})
})
it('should handle subdomain conflicts', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
const mockChat = {
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
}
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
// Mock subdomain conflict
mockLimit.mockResolvedValueOnce([{ id: 'other-chat-id', subdomain: 'new-subdomain' }])
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ subdomain: 'new-subdomain' }),
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(400)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Subdomain already in use', 400)
})
it('should validate password requirement for password auth', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
const mockChat = {
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
authType: 'public',
password: null,
}
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ authType: 'password' }), // No password provided
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(400)
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
'Password is required when using password protection',
400
)
})
it('should allow access when user has workspace admin permission', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'admin-user-id' },
}),
}))
const mockChat = {
id: 'chat-123',
subdomain: 'test-chat',
title: 'Test Chat',
authType: 'public',
}
// User doesn't own chat but has workspace admin access
mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'PATCH',
body: JSON.stringify({ title: 'Admin Updated Chat' }),
})
const { PATCH } = await import('./route')
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(200)
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'admin-user-id')
})
})
describe('DELETE', () => {
it('should return 401 when user is not authenticated', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue(null),
}))
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'DELETE',
})
const { DELETE } = await import('./route')
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(401)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unauthorized', 401)
})
it('should return 404 when chat not found or access denied', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
mockCheckChatAccess.mockResolvedValue({ hasAccess: false })
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'DELETE',
})
const { DELETE } = await import('./route')
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(404)
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Chat not found or access denied', 404)
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'user-id')
})
it('should delete chat when user has access', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'user-id' },
}),
}))
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
mockWhere.mockResolvedValue(undefined)
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'DELETE',
})
const { DELETE } = await import('./route')
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(200)
expect(mockDelete).toHaveBeenCalled()
expect(mockCreateSuccessResponse).toHaveBeenCalledWith({
message: 'Chat deployment deleted successfully',
})
})
it('should allow deletion when user has workspace admin permission', async () => {
vi.doMock('@/lib/auth', () => ({
getSession: vi.fn().mockResolvedValue({
user: { id: 'admin-user-id' },
}),
}))
// User doesn't own chat but has workspace admin access
mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
mockWhere.mockResolvedValue(undefined)
const req = new NextRequest('http://localhost:3000/api/chat/edit/chat-123', {
method: 'DELETE',
})
const { DELETE } = await import('./route')
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
expect(response.status).toBe(200)
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'admin-user-id')
expect(mockDelete).toHaveBeenCalled()
})
})
})