mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(permissions): fixed permissions and variables access patterns (#539)
* fixed permissions and variables access patterns * only fetch variables if we are actually switching worklfows * fixed tests
This commit is contained in:
644
apps/sim/app/api/workflows/[id]/route.test.ts
Normal file
644
apps/sim/app/api/workflows/[id]/route.test.ts
Normal file
@@ -0,0 +1,644 @@
|
||||
/**
|
||||
* Integration tests for workflow by ID API route
|
||||
* Tests the new centralized permissions system
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('Workflow By ID API Route', () => {
|
||||
const mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('mock-request-id-12345678'),
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('GET /api/workflows/[id]', () => {
|
||||
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/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it('should return 404 when workflow does not exist', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/nonexistent')
|
||||
const params = Promise.resolve({ id: 'nonexistent' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Workflow not found')
|
||||
})
|
||||
|
||||
it.concurrent('should allow access when user owns the workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: null,
|
||||
state: { blocks: {}, edges: [] },
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data.id).toBe('workflow-123')
|
||||
})
|
||||
|
||||
it.concurrent('should allow access when user has workspace permissions', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
state: { blocks: {}, edges: [] },
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('read'),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data.id).toBe('workflow-123')
|
||||
})
|
||||
|
||||
it('should deny access when user has no workspace permissions', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
state: { blocks: {}, edges: [] },
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue(null),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Access denied')
|
||||
})
|
||||
|
||||
it.concurrent('should use normalized tables when available', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: null,
|
||||
state: { blocks: {}, edges: [] },
|
||||
}
|
||||
|
||||
const mockNormalizedData = {
|
||||
blocks: { 'block-1': { id: 'block-1', type: 'starter' } },
|
||||
edges: [{ id: 'edge-1', source: 'block-1', target: 'block-2' }],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workflows/db-helpers', () => ({
|
||||
loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data.state.blocks).toEqual(mockNormalizedData.blocks)
|
||||
expect(data.data.state.edges).toEqual(mockNormalizedData.edges)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/workflows/[id]', () => {
|
||||
it('should allow owner to delete workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: null,
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
|
||||
await callback({
|
||||
delete: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
transaction: mockTransaction,
|
||||
},
|
||||
}))
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow admin to delete workspace workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
|
||||
await callback({
|
||||
delete: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
transaction: mockTransaction,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(true),
|
||||
}))
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should deny deletion for non-admin users', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('read'),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { DELETE } = await import('./route')
|
||||
const response = await DELETE(req, { params })
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Access denied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /api/workflows/[id]', () => {
|
||||
it('should allow owner to update workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: null,
|
||||
}
|
||||
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
update: vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([{ ...mockWorkflow, ...updateData }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { PUT } = await import('./route')
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.workflow.name).toBe('Updated Workflow')
|
||||
})
|
||||
|
||||
it('should allow users with write permission to update workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
update: vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([{ ...mockWorkflow, ...updateData }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('write'),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { PUT } = await import('./route')
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.workflow.name).toBe('Updated Workflow')
|
||||
})
|
||||
|
||||
it('should deny update for users with only read permission', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('read'),
|
||||
hasAdminPermission: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { PUT } = await import('./route')
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Access denied')
|
||||
})
|
||||
|
||||
it.concurrent('should validate request data', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: null,
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockResolvedValue(mockWorkflow),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
// Invalid data - empty name
|
||||
const invalidData = { name: '' }
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(invalidData),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { PUT } = await import('./route')
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Invalid request data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', () => {
|
||||
it.concurrent('should handle database errors gracefully', async () => {
|
||||
vi.doMock('@/lib/auth', () => ({
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/db', () => ({
|
||||
db: {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
then: vi.fn().mockRejectedValue(new Error('Database connection timeout')),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Internal server error')
|
||||
expect(mockLogger.error).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,21 +1,15 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { db } from '@/db'
|
||||
import {
|
||||
workflow,
|
||||
workflowBlocks,
|
||||
workflowEdges,
|
||||
workflowSubflows,
|
||||
workspaceMember,
|
||||
} from '@/db/schema'
|
||||
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('WorkflowByIdAPI')
|
||||
|
||||
// Schema for workflow metadata updates
|
||||
const UpdateWorkflowSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').optional(),
|
||||
description: z.string().optional(),
|
||||
@@ -63,20 +57,14 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
hasAccess = true
|
||||
}
|
||||
|
||||
// Case 2: Workflow belongs to a workspace the user is a member of
|
||||
// Case 2: Workflow belongs to a workspace the user has permissions for
|
||||
if (!hasAccess && workflowData.workspaceId) {
|
||||
const membership = await db
|
||||
.select({ id: workspaceMember.id })
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workflowData.workspaceId),
|
||||
eq(workspaceMember.userId, userId)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (membership) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
userId,
|
||||
'workspace',
|
||||
workflowData.workspaceId
|
||||
)
|
||||
if (userPermission !== null) {
|
||||
hasAccess = true
|
||||
}
|
||||
}
|
||||
@@ -182,20 +170,10 @@ export async function DELETE(
|
||||
canDelete = true
|
||||
}
|
||||
|
||||
// Case 2: Workflow belongs to a workspace and user has admin/owner role
|
||||
// Case 2: Workflow belongs to a workspace and user has admin permission
|
||||
if (!canDelete && workflowData.workspaceId) {
|
||||
const membership = await db
|
||||
.select({ role: workspaceMember.role })
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workflowData.workspaceId),
|
||||
eq(workspaceMember.userId, userId)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (membership && (membership.role === 'owner' || membership.role === 'admin')) {
|
||||
const hasAdmin = await hasAdminPermission(userId, workflowData.workspaceId)
|
||||
if (hasAdmin) {
|
||||
canDelete = true
|
||||
}
|
||||
}
|
||||
@@ -300,20 +278,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
canUpdate = true
|
||||
}
|
||||
|
||||
// Case 2: Workflow belongs to a workspace and user has admin/owner role
|
||||
// Case 2: Workflow belongs to a workspace and user has write or admin permission
|
||||
if (!canUpdate && workflowData.workspaceId) {
|
||||
const membership = await db
|
||||
.select({ role: workspaceMember.role })
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workflowData.workspaceId),
|
||||
eq(workspaceMember.userId, userId)
|
||||
)
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (membership && (membership.role === 'owner' || membership.role === 'admin')) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
userId,
|
||||
'workspace',
|
||||
workflowData.workspaceId
|
||||
)
|
||||
if (userPermission === 'write' || userPermission === 'admin') {
|
||||
canUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
325
apps/sim/app/api/workflows/[id]/variables/route.test.ts
Normal file
325
apps/sim/app/api/workflows/[id]/variables/route.test.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Tests for workflow variables API route
|
||||
* Tests the optimized permissions and caching system
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createMockDatabase,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockUser,
|
||||
setupCommonApiMocks,
|
||||
} from '@/app/api/__test-utils__/utils'
|
||||
|
||||
describe('Workflow Variables API Route', () => {
|
||||
let authMocks: ReturnType<typeof mockAuth>
|
||||
let databaseMocks: ReturnType<typeof createMockDatabase>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
setupCommonApiMocks()
|
||||
mockCryptoUuid('mock-request-id-12345678')
|
||||
authMocks = mockAuth(mockUser)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('GET /api/workflows/[id]/variables', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
authMocks.setUnauthenticated()
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it('should return 404 when workflow does not exist', async () => {
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[]] }, // No workflow found
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/nonexistent/variables')
|
||||
const params = Promise.resolve({ id: 'nonexistent' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Workflow not found')
|
||||
})
|
||||
|
||||
it('should allow access when user owns the workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
workspaceId: null,
|
||||
variables: {
|
||||
'var-1': { id: 'var-1', name: 'test', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data).toEqual(mockWorkflow.variables)
|
||||
})
|
||||
|
||||
it('should allow access when user has workspace permissions', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
workspaceId: 'workspace-456',
|
||||
variables: {
|
||||
'var-1': { id: 'var-1', name: 'test', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('read'),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data).toEqual(mockWorkflow.variables)
|
||||
|
||||
// Verify permissions check was called
|
||||
const { getUserEntityPermissions } = await import('@/lib/permissions/utils')
|
||||
expect(getUserEntityPermissions).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
'workspace',
|
||||
'workspace-456'
|
||||
)
|
||||
})
|
||||
|
||||
it('should deny access when user has no workspace permissions', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
workspaceId: 'workspace-456',
|
||||
variables: {},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it.concurrent('should include proper cache headers', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
workspaceId: null,
|
||||
variables: {
|
||||
'var-1': { id: 'var-1', name: 'test', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('Cache-Control')).toBe('max-age=30, stale-while-revalidate=300')
|
||||
expect(response.headers.get('ETag')).toMatch(/^"variables-workflow-123-\d+"$/)
|
||||
})
|
||||
|
||||
it.concurrent('should return empty object for workflows with no variables', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
workspaceId: null,
|
||||
variables: null,
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.data).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/workflows/[id]/variables', () => {
|
||||
it('should allow owner to update variables', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
workspaceId: null,
|
||||
variables: {},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
update: { results: [{}] },
|
||||
})
|
||||
|
||||
const variables = [
|
||||
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
|
||||
]
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ variables }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny access for users without permissions', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'other-user',
|
||||
workspaceId: 'workspace-456',
|
||||
variables: {},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
const variables = [
|
||||
{ id: 'var-1', workflowId: 'workflow-123', name: 'test', type: 'string', value: 'hello' },
|
||||
]
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ variables }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it.concurrent('should validate request data schema', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
workspaceId: null,
|
||||
variables: {},
|
||||
}
|
||||
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { results: [[mockWorkflow]] },
|
||||
})
|
||||
|
||||
// Invalid data - missing required fields
|
||||
const invalidData = { variables: [{ name: 'test' }] }
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(invalidData),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req, { params })
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Invalid request data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', () => {
|
||||
it.concurrent('should handle database errors gracefully', async () => {
|
||||
authMocks.setAuthenticated({ id: 'user-123', email: 'test@example.com' })
|
||||
databaseMocks = createMockDatabase({
|
||||
select: { throwError: true, errorMessage: 'Database connection failed' },
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123/variables')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const { GET } = await import('./route')
|
||||
const response = await GET(req, { params })
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Database connection failed')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow, workspaceMember } from '@/db/schema'
|
||||
import { workflow } from '@/db/schema'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
|
||||
const logger = createLogger('WorkflowVariablesAPI')
|
||||
@@ -47,23 +48,17 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const workflowData = workflowRecord[0]
|
||||
const workspaceId = workflowData.workspaceId
|
||||
|
||||
// Check authorization - either the user owns the workflow or is a member of the workspace
|
||||
// Check authorization - either the user owns the workflow or has workspace permissions
|
||||
let isAuthorized = workflowData.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace membership
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workspaceId) {
|
||||
const membership = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
isAuthorized = membership.length > 0
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workspaceId
|
||||
)
|
||||
isAuthorized = userPermission !== null
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
@@ -151,23 +146,17 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const workflowData = workflowRecord[0]
|
||||
const workspaceId = workflowData.workspaceId
|
||||
|
||||
// Check authorization - either the user owns the workflow or is a member of the workspace
|
||||
// Check authorization - either the user owns the workflow or has workspace permissions
|
||||
let isAuthorized = workflowData.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace membership
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workspaceId) {
|
||||
const membership = await db
|
||||
.select()
|
||||
.from(workspaceMember)
|
||||
.where(
|
||||
and(
|
||||
eq(workspaceMember.workspaceId, workspaceId),
|
||||
eq(workspaceMember.userId, session.user.id)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
isAuthorized = membership.length > 0
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workspaceId
|
||||
)
|
||||
isAuthorized = userPermission !== null
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
@@ -181,9 +170,10 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
const variables = (workflowData.variables as Record<string, Variable>) || {}
|
||||
|
||||
// Add cache headers to prevent frequent reloading
|
||||
const variableHash = JSON.stringify(variables).length
|
||||
const headers = new Headers({
|
||||
'Cache-Control': 'max-age=60, stale-while-revalidate=300', // Cache for 1 minute, stale for 5
|
||||
ETag: `"${requestId}-${Object.keys(variables).length}"`,
|
||||
'Cache-Control': 'max-age=30, stale-while-revalidate=300', // Cache for 30 seconds, stale for 5 min
|
||||
ETag: `"variables-${workflowId}-${variableHash}"`,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -42,12 +42,12 @@ export function Variables({ panelWidth }: VariablesProps) {
|
||||
// Get variables for the current workflow
|
||||
const workflowVariables = activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []
|
||||
|
||||
// Load variables when workflow changes
|
||||
// Load variables when active workflow changes
|
||||
useEffect(() => {
|
||||
if (activeWorkflowId && workflows[activeWorkflowId]) {
|
||||
if (activeWorkflowId) {
|
||||
loadVariables(activeWorkflowId)
|
||||
}
|
||||
}, [activeWorkflowId, workflows, loadVariables])
|
||||
}, [activeWorkflowId, loadVariables])
|
||||
|
||||
// Track editor references
|
||||
const editorRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
|
||||
@@ -841,16 +841,15 @@ const WorkflowContent = React.memo(() => {
|
||||
return
|
||||
}
|
||||
|
||||
// Reset variables loaded state before setting active workflow
|
||||
resetVariablesLoaded()
|
||||
|
||||
// Always call setActiveWorkflow when workflow ID changes to ensure proper state
|
||||
const { activeWorkflowId } = useWorkflowRegistry.getState()
|
||||
|
||||
if (activeWorkflowId !== currentId) {
|
||||
// Only reset variables when actually switching workflows
|
||||
resetVariablesLoaded()
|
||||
setActiveWorkflow(currentId)
|
||||
} else {
|
||||
// Even if the workflow is already active, call setActiveWorkflow to ensure state consistency
|
||||
// Don't reset variables cache if we're not actually switching workflows
|
||||
setActiveWorkflow(currentId)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Socket Integration Test</title>
|
||||
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 5px; }
|
||||
.connected { background-color: #d4edda; color: #155724; }
|
||||
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||
.log { background-color: #f8f9fa; padding: 10px; margin: 10px 0; border-radius: 5px; max-height: 300px; overflow-y: auto; }
|
||||
button { padding: 10px 20px; margin: 5px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; }
|
||||
button:hover { background-color: #0056b3; }
|
||||
button:disabled { background-color: #6c757d; cursor: not-allowed; }
|
||||
input { padding: 8px; margin: 5px; border: 1px solid #ccc; border-radius: 3px; }
|
||||
.presence { background-color: #e7f3ff; padding: 10px; margin: 10px 0; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Socket.IO Collaborative Workflow Test</h1>
|
||||
|
||||
<div id="status" class="status disconnected">
|
||||
Disconnected
|
||||
</div>
|
||||
|
||||
<div class="presence">
|
||||
<h3>Presence Users:</h3>
|
||||
<div id="presence-users">None</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Test Workflow Operations:</h3>
|
||||
<input type="text" id="workflow-id" placeholder="Workflow ID" value="test-workflow-123">
|
||||
<button onclick="joinWorkflow()">Join Workflow</button>
|
||||
<button onclick="leaveWorkflow()">Leave Workflow</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Block Operations:</h3>
|
||||
<button onclick="addTestBlock()">Add Test Block</button>
|
||||
<button onclick="removeTestBlock()">Remove Test Block</button>
|
||||
<button onclick="updateBlockPosition()">Update Block Position</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Edge Operations:</h3>
|
||||
<button onclick="addTestEdge()">Add Test Edge</button>
|
||||
<button onclick="removeTestEdge()">Remove Test Edge</button>
|
||||
</div>
|
||||
|
||||
<div class="log">
|
||||
<h3>Event Log:</h3>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let socket = null;
|
||||
let currentWorkflowId = null;
|
||||
let testBlockId = 'test-block-' + Math.random().toString(36).substr(2, 9);
|
||||
let testEdgeId = 'test-edge-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
function log(message) {
|
||||
const logDiv = document.getElementById('log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logDiv.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(connected) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
if (connected) {
|
||||
statusDiv.className = 'status connected';
|
||||
statusDiv.textContent = 'Connected to Socket.IO server';
|
||||
} else {
|
||||
statusDiv.className = 'status disconnected';
|
||||
statusDiv.textContent = 'Disconnected from Socket.IO server';
|
||||
}
|
||||
}
|
||||
|
||||
function updatePresence(users) {
|
||||
const presenceDiv = document.getElementById('presence-users');
|
||||
if (users.length === 0) {
|
||||
presenceDiv.textContent = 'None';
|
||||
} else {
|
||||
presenceDiv.innerHTML = users.map(user =>
|
||||
`<div>• ${user.userName} (${user.userId})</div>`
|
||||
).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize socket connection
|
||||
function initSocket() {
|
||||
socket = io('http://localhost:3002', {
|
||||
withCredentials: false, // Disable for testing
|
||||
transports: ['polling', 'websocket'],
|
||||
forceNew: true,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
log('✅ Connected to socket server');
|
||||
updateStatus(true);
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
log(`❌ Disconnected: ${reason}`);
|
||||
updateStatus(false);
|
||||
updatePresence([]);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
log(`🚨 Socket error: ${JSON.stringify(error)}`);
|
||||
});
|
||||
|
||||
socket.on('workflow-operation', (data) => {
|
||||
log(`📝 Received operation: ${data.operation} on ${data.target} from ${data.userName || data.userId}`);
|
||||
log(` Payload: ${JSON.stringify(data.payload)}`);
|
||||
});
|
||||
|
||||
socket.on('user-joined', (data) => {
|
||||
log(`👋 User joined: ${data.userName || data.userId}`);
|
||||
});
|
||||
|
||||
socket.on('user-left', (data) => {
|
||||
log(`👋 User left: ${data.userId}`);
|
||||
});
|
||||
|
||||
socket.on('presence-update', (users) => {
|
||||
log(`👥 Presence update: ${users.length} users`);
|
||||
updatePresence(users);
|
||||
});
|
||||
|
||||
socket.on('operation-confirmed', (data) => {
|
||||
log(`✅ Operation confirmed: ${data.operation} on ${data.target}`);
|
||||
});
|
||||
|
||||
socket.on('operation-error', (error) => {
|
||||
log(`❌ Operation error: ${error.message || error.error}`);
|
||||
});
|
||||
|
||||
socket.on('operation-forbidden', (error) => {
|
||||
log(`🚫 Operation forbidden: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
function joinWorkflow() {
|
||||
const workflowId = document.getElementById('workflow-id').value;
|
||||
if (!workflowId || !socket) return;
|
||||
|
||||
currentWorkflowId = workflowId;
|
||||
socket.emit('join-workflow', { workflowId });
|
||||
log(`🔗 Joining workflow: ${workflowId}`);
|
||||
}
|
||||
|
||||
function leaveWorkflow() {
|
||||
if (!socket) return;
|
||||
socket.emit('leave-workflow');
|
||||
currentWorkflowId = null;
|
||||
log('🔗 Left workflow');
|
||||
updatePresence([]);
|
||||
}
|
||||
|
||||
function addTestBlock() {
|
||||
if (!socket || !currentWorkflowId) {
|
||||
log('❌ Not connected to a workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: 'add',
|
||||
target: 'block',
|
||||
payload: {
|
||||
id: testBlockId,
|
||||
type: 'action',
|
||||
name: 'Test Block',
|
||||
position: { x: Math.random() * 400, y: Math.random() * 300 },
|
||||
data: {}
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
socket.emit('workflow-operation', operation);
|
||||
log(`📤 Sent add block operation: ${testBlockId}`);
|
||||
}
|
||||
|
||||
function removeTestBlock() {
|
||||
if (!socket || !currentWorkflowId) {
|
||||
log('❌ Not connected to a workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: 'remove',
|
||||
target: 'block',
|
||||
payload: { id: testBlockId },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
socket.emit('workflow-operation', operation);
|
||||
log(`📤 Sent remove block operation: ${testBlockId}`);
|
||||
}
|
||||
|
||||
function updateBlockPosition() {
|
||||
if (!socket || !currentWorkflowId) {
|
||||
log('❌ Not connected to a workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: 'update-position',
|
||||
target: 'block',
|
||||
payload: {
|
||||
id: testBlockId,
|
||||
position: { x: Math.random() * 400, y: Math.random() * 300 }
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
socket.emit('workflow-operation', operation);
|
||||
log(`📤 Sent update position operation: ${testBlockId}`);
|
||||
}
|
||||
|
||||
function addTestEdge() {
|
||||
if (!socket || !currentWorkflowId) {
|
||||
log('❌ Not connected to a workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: 'add',
|
||||
target: 'edge',
|
||||
payload: {
|
||||
id: testEdgeId,
|
||||
source: 'source-block-id',
|
||||
target: 'target-block-id',
|
||||
sourceHandle: 'source',
|
||||
targetHandle: 'target'
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
socket.emit('workflow-operation', operation);
|
||||
log(`📤 Sent add edge operation: ${testEdgeId}`);
|
||||
}
|
||||
|
||||
function removeTestEdge() {
|
||||
if (!socket || !currentWorkflowId) {
|
||||
log('❌ Not connected to a workflow');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: 'remove',
|
||||
target: 'edge',
|
||||
payload: { id: testEdgeId },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
socket.emit('workflow-operation', operation);
|
||||
log(`📤 Sent remove edge operation: ${testEdgeId}`);
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
window.onload = () => {
|
||||
log('🚀 Initializing socket connection...');
|
||||
initSocket();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user