Skip to content

Commit bb589df

Browse files
committed
added tests
1 parent 153cb09 commit bb589df

File tree

6 files changed

+619
-56
lines changed

6 files changed

+619
-56
lines changed
-138 KB
Binary file not shown.
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/**
2+
* Tests for schedule reactivate PUT API route
3+
*
4+
* @vitest-environment node
5+
*/
6+
import { NextRequest } from 'next/server'
7+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8+
9+
const { mockGetSession, mockGetUserEntityPermissions, mockDbSelect, mockDbUpdate } = vi.hoisted(
10+
() => ({
11+
mockGetSession: vi.fn(),
12+
mockGetUserEntityPermissions: vi.fn(),
13+
mockDbSelect: vi.fn(),
14+
mockDbUpdate: vi.fn(),
15+
})
16+
)
17+
18+
vi.mock('@/lib/auth', () => ({
19+
getSession: mockGetSession,
20+
}))
21+
22+
vi.mock('@/lib/workspaces/permissions/utils', () => ({
23+
getUserEntityPermissions: mockGetUserEntityPermissions,
24+
}))
25+
26+
vi.mock('@sim/db', () => ({
27+
db: {
28+
select: mockDbSelect,
29+
update: mockDbUpdate,
30+
},
31+
}))
32+
33+
vi.mock('@sim/db/schema', () => ({
34+
workflow: { id: 'id', userId: 'userId', workspaceId: 'workspaceId' },
35+
workflowSchedule: { id: 'id', workflowId: 'workflowId', status: 'status' },
36+
}))
37+
38+
vi.mock('drizzle-orm', () => ({
39+
eq: vi.fn(),
40+
}))
41+
42+
vi.mock('@/lib/core/utils/request', () => ({
43+
generateRequestId: () => 'test-request-id',
44+
}))
45+
46+
vi.mock('@/lib/logs/console/logger', () => ({
47+
createLogger: () => ({
48+
info: vi.fn(),
49+
warn: vi.fn(),
50+
error: vi.fn(),
51+
debug: vi.fn(),
52+
}),
53+
}))
54+
55+
import { PUT } from '@/app/api/schedules/[id]/route'
56+
57+
function createRequest(body: Record<string, unknown>): NextRequest {
58+
return new NextRequest(new URL('http://test/api/schedules/sched-1'), {
59+
method: 'PUT',
60+
body: JSON.stringify(body),
61+
headers: { 'Content-Type': 'application/json' },
62+
})
63+
}
64+
65+
function createParams(id: string): { params: Promise<{ id: string }> } {
66+
return { params: Promise.resolve({ id }) }
67+
}
68+
69+
function mockDbChain(selectResults: unknown[][]) {
70+
let selectCallIndex = 0
71+
mockDbSelect.mockImplementation(() => ({
72+
from: () => ({
73+
where: () => ({
74+
limit: () => selectResults[selectCallIndex++] || [],
75+
}),
76+
}),
77+
}))
78+
79+
mockDbUpdate.mockImplementation(() => ({
80+
set: () => ({
81+
where: vi.fn().mockResolvedValue({}),
82+
}),
83+
}))
84+
}
85+
86+
describe('Schedule PUT API (Reactivate)', () => {
87+
beforeEach(() => {
88+
vi.clearAllMocks()
89+
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
90+
mockGetUserEntityPermissions.mockResolvedValue('write')
91+
})
92+
93+
afterEach(() => {
94+
vi.clearAllMocks()
95+
})
96+
97+
describe('Authentication', () => {
98+
it('returns 401 when user is not authenticated', async () => {
99+
mockGetSession.mockResolvedValue(null)
100+
101+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
102+
103+
expect(res.status).toBe(401)
104+
const data = await res.json()
105+
expect(data.error).toBe('Unauthorized')
106+
})
107+
})
108+
109+
describe('Request Validation', () => {
110+
it('returns 400 when action is not reactivate', async () => {
111+
mockDbChain([
112+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
113+
[{ userId: 'user-1', workspaceId: null }],
114+
])
115+
116+
const res = await PUT(createRequest({ action: 'disable' }), createParams('sched-1'))
117+
118+
expect(res.status).toBe(400)
119+
const data = await res.json()
120+
expect(data.error).toBe('Invalid request body')
121+
})
122+
123+
it('returns 400 when action is missing', async () => {
124+
mockDbChain([
125+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
126+
[{ userId: 'user-1', workspaceId: null }],
127+
])
128+
129+
const res = await PUT(createRequest({}), createParams('sched-1'))
130+
131+
expect(res.status).toBe(400)
132+
const data = await res.json()
133+
expect(data.error).toBe('Invalid request body')
134+
})
135+
})
136+
137+
describe('Schedule Not Found', () => {
138+
it('returns 404 when schedule does not exist', async () => {
139+
mockDbChain([[]])
140+
141+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-999'))
142+
143+
expect(res.status).toBe(404)
144+
const data = await res.json()
145+
expect(data.error).toBe('Schedule not found')
146+
})
147+
148+
it('returns 404 when workflow does not exist for schedule', async () => {
149+
mockDbChain([[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }], []])
150+
151+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
152+
153+
expect(res.status).toBe(404)
154+
const data = await res.json()
155+
expect(data.error).toBe('Workflow not found')
156+
})
157+
})
158+
159+
describe('Authorization', () => {
160+
it('returns 403 when user is not workflow owner', async () => {
161+
mockDbChain([
162+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
163+
[{ userId: 'other-user', workspaceId: null }],
164+
])
165+
166+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
167+
168+
expect(res.status).toBe(403)
169+
const data = await res.json()
170+
expect(data.error).toBe('Not authorized to modify this schedule')
171+
})
172+
173+
it('returns 403 for workspace member with only read permission', async () => {
174+
mockGetUserEntityPermissions.mockResolvedValue('read')
175+
mockDbChain([
176+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
177+
[{ userId: 'other-user', workspaceId: 'ws-1' }],
178+
])
179+
180+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
181+
182+
expect(res.status).toBe(403)
183+
})
184+
185+
it('allows workflow owner to reactivate', async () => {
186+
mockDbChain([
187+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
188+
[{ userId: 'user-1', workspaceId: null }],
189+
])
190+
191+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
192+
193+
expect(res.status).toBe(200)
194+
const data = await res.json()
195+
expect(data.message).toBe('Schedule activated successfully')
196+
})
197+
198+
it('allows workspace member with write permission to reactivate', async () => {
199+
mockGetUserEntityPermissions.mockResolvedValue('write')
200+
mockDbChain([
201+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
202+
[{ userId: 'other-user', workspaceId: 'ws-1' }],
203+
])
204+
205+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
206+
207+
expect(res.status).toBe(200)
208+
})
209+
210+
it('allows workspace admin to reactivate', async () => {
211+
mockGetUserEntityPermissions.mockResolvedValue('admin')
212+
mockDbChain([
213+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
214+
[{ userId: 'other-user', workspaceId: 'ws-1' }],
215+
])
216+
217+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
218+
219+
expect(res.status).toBe(200)
220+
})
221+
})
222+
223+
describe('Schedule State Handling', () => {
224+
it('returns success message when schedule is already active', async () => {
225+
mockDbChain([
226+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'active' }],
227+
[{ userId: 'user-1', workspaceId: null }],
228+
])
229+
230+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
231+
232+
expect(res.status).toBe(200)
233+
const data = await res.json()
234+
expect(data.message).toBe('Schedule is already active')
235+
expect(mockDbUpdate).not.toHaveBeenCalled()
236+
})
237+
238+
it('successfully reactivates disabled schedule', async () => {
239+
mockDbChain([
240+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
241+
[{ userId: 'user-1', workspaceId: null }],
242+
])
243+
244+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
245+
246+
expect(res.status).toBe(200)
247+
const data = await res.json()
248+
expect(data.message).toBe('Schedule activated successfully')
249+
expect(data.nextRunAt).toBeDefined()
250+
expect(mockDbUpdate).toHaveBeenCalled()
251+
})
252+
253+
it('sets nextRunAt to approximately 1 minute in future', async () => {
254+
mockDbChain([
255+
[{ id: 'sched-1', workflowId: 'wf-1', status: 'disabled' }],
256+
[{ userId: 'user-1', workspaceId: null }],
257+
])
258+
259+
const beforeCall = Date.now()
260+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
261+
const afterCall = Date.now()
262+
263+
expect(res.status).toBe(200)
264+
const data = await res.json()
265+
const nextRunAt = new Date(data.nextRunAt).getTime()
266+
267+
// nextRunAt should be ~60 seconds from now
268+
expect(nextRunAt).toBeGreaterThanOrEqual(beforeCall + 60000 - 1000)
269+
expect(nextRunAt).toBeLessThanOrEqual(afterCall + 60000 + 1000)
270+
})
271+
})
272+
273+
describe('Error Handling', () => {
274+
it('returns 500 when database operation fails', async () => {
275+
mockDbSelect.mockImplementation(() => {
276+
throw new Error('Database connection failed')
277+
})
278+
279+
const res = await PUT(createRequest({ action: 'reactivate' }), createParams('sched-1'))
280+
281+
expect(res.status).toBe(500)
282+
const data = await res.json()
283+
expect(data.error).toBe('Failed to update schedule')
284+
})
285+
})
286+
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { createLogger } from '@/lib/logs/console/logger'
1919
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
2020
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
2121
import { startsWithUuid } from '@/executor/constants'
22-
import { useNotificationStore } from '@/stores/notifications/store'
2322
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2423
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2524
import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -69,7 +68,6 @@ export function DeployModal({
6968
)
7069
const isDeployed = deploymentStatus?.isDeployed ?? isDeployedProp
7170
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
72-
const addNotification = useNotificationStore((state) => state.addNotification)
7371
const [isSubmitting, setIsSubmitting] = useState(false)
7472
const [isUndeploying, setIsUndeploying] = useState(false)
7573
const [deploymentInfo, setDeploymentInfo] = useState<WorkflowDeploymentInfo | null>(null)
@@ -260,14 +258,7 @@ export function DeployModal({
260258
} catch (error: unknown) {
261259
logger.error('Error deploying workflow:', { error })
262260
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
263-
264-
// Close modal and show notification for deploy errors
265-
onOpenChange(false)
266-
addNotification({
267-
level: 'error',
268-
message: errorMessage,
269-
workflowId: workflowId || undefined,
270-
})
261+
setApiDeployError(errorMessage)
271262
} finally {
272263
setIsSubmitting(false)
273264
}
@@ -474,14 +465,7 @@ export function DeployModal({
474465
} catch (error: unknown) {
475466
logger.error('Error redeploying workflow:', { error })
476467
const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow'
477-
478-
// Close modal and show notification for redeploy errors
479-
onOpenChange(false)
480-
addNotification({
481-
level: 'error',
482-
message: errorMessage,
483-
workflowId: workflowId || undefined,
484-
})
468+
setApiDeployError(errorMessage)
485469
} finally {
486470
setIsSubmitting(false)
487471
}

0 commit comments

Comments
 (0)