Files
windmill/frontend/src/lib/utils_draft_deploy.test.ts
T
Ruben Fiszel 2b525d28db fix: honor on-behalf-of when a workflow step dispatches a script or flow (#10437)
* fix: run on-behalf-of scripts under their own identity from workflow steps

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: correct the on-behalf-of provenance comment in the script draft deploy

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: pin preserve_on_behalf_of forwarding in the script and flow draft deploys

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: drop the historical comparison from the draft-deploy test comment

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:29:30 +02:00

127 lines
5.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { draftBaseIsStale, deployDraft } from './utils_draft_deploy'
vi.mock('$lib/gen', () => ({
ScriptService: { getScriptByPath: vi.fn(), createScript: vi.fn() },
FlowService: { getFlowByPath: vi.fn(), createFlow: vi.fn(), updateFlow: vi.fn() },
DraftService: { deleteDraft: vi.fn() },
AppService: {},
VariableService: {},
ResourceService: {},
ScheduleService: {},
HttpTriggerService: {},
WebsocketTriggerService: {},
PostgresTriggerService: {},
KafkaTriggerService: {},
NatsTriggerService: {},
MqttTriggerService: {},
AmqpTriggerService: {},
SqsTriggerService: {},
GcpTriggerService: {},
AzureTriggerService: {},
EmailTriggerService: {}
}))
vi.mock('$lib/userDraftDbSyncer.svelte', () => ({ UserDraftDbSyncer: { save: vi.fn() } }))
vi.mock('$lib/workspaceDrafts.svelte', () => ({ invalidateWorkspaceDrafts: vi.fn() }))
vi.mock('$lib/workspaceComparison', () => ({ invalidateWorkspaceComparison: vi.fn() }))
vi.mock('$lib/localDraftHints.svelte', () => ({ setLocalDraftHint: vi.fn() }))
vi.mock('$lib/rawAppDeploy', () => ({ deployRawAppDraft: vi.fn() }))
vi.mock('$lib/components/raw_apps/utils', () => ({ canonicalRawAppDiffValue: vi.fn() }))
vi.mock('$lib/appDiffSides', () => ({ classicAppDraftParts: vi.fn() }))
vi.mock('$lib/utils_deployable', () => ({ TRIGGER_RUNTIME_IGNORE: [] }))
import { ScriptService, FlowService } from '$lib/gen'
// draftBaseIsStale compares a draft's base pointer against the deployed head
// of the item it was fetched with (`get_draft=true`). Shared by CompareDrafts
// and the session Edits drawer — a regression here silently hides (or
// fabricates) the "started from an older deployed version" warning.
describe('draftBaseIsStale', () => {
it('script: stale iff the draft parent_hash differs from the deployed hash', () => {
expect(draftBaseIsStale('script', { hash: 'v2', draft: { parent_hash: 'v1' } })).toBe(true)
expect(draftBaseIsStale('script', { hash: 'v2', draft: { parent_hash: 'v2' } })).toBe(false)
})
it('script: no base pointer or no head → not stale (nothing to compare)', () => {
expect(draftBaseIsStale('script', { hash: 'v2', draft: {} })).toBe(false)
expect(draftBaseIsStale('script', { draft: { parent_hash: 'v1' } })).toBe(false)
})
it('flow: compares the pinned version_id against the deployed head', () => {
expect(draftBaseIsStale('flow', { version_id: 7, draft: { version_id: 5 } })).toBe(true)
expect(draftBaseIsStale('flow', { version_id: 7, draft: { version_id: 7 } })).toBe(false)
expect(draftBaseIsStale('flow', { version_id: 7, draft: {} })).toBe(false)
})
it('app/raw_app: compares parent_version against the last of versions', () => {
expect(draftBaseIsStale('app', { versions: [1, 2, 3], draft: { parent_version: 2 } })).toBe(
true
)
expect(draftBaseIsStale('raw_app', { versions: [1, 2, 3], draft: { parent_version: 3 } })).toBe(
false
)
expect(draftBaseIsStale('app', { versions: [], draft: { parent_version: 2 } })).toBe(false)
})
it('no draft on the response → not stale', () => {
expect(draftBaseIsStale('script', { hash: 'v2' })).toBe(false)
expect(draftBaseIsStale('script', undefined)).toBe(false)
})
})
// Without preserve_on_behalf_of the backend rewrites on_behalf_of_email to the
// deploying user, so deploying a draft silently re-points the runnable's
// identity.
describe('deployDraft preserves on_behalf_of', () => {
beforeEach(() => vi.clearAllMocks())
it('script: forwards the flag when the draft carries an on_behalf_of_email', async () => {
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
hash: 'v1',
draft: { path: 'f/admin/send_email', on_behalf_of_email: 'alice@windmill.dev' }
} as any)
expect(await deployDraft('script', 'f/admin/send_email', 'ws')).toEqual({ success: true })
expect(ScriptService.createScript).toHaveBeenCalledWith(
expect.objectContaining({
requestBody: expect.objectContaining({
on_behalf_of_email: 'alice@windmill.dev',
preserve_on_behalf_of: true
})
})
)
})
it('script: omits the flag when the draft has no on_behalf_of_email', async () => {
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
hash: 'v1',
draft: { path: 'f/admin/send_email' }
} as any)
await deployDraft('script', 'f/admin/send_email', 'ws')
expect(ScriptService.createScript).toHaveBeenCalledWith(
expect.objectContaining({
requestBody: expect.objectContaining({ preserve_on_behalf_of: undefined })
})
)
})
it('flow: forwards the flag when the draft carries an on_behalf_of_email', async () => {
vi.mocked(FlowService.getFlowByPath).mockResolvedValueOnce({
draft: { path: 'f/admin/notify', value: {}, on_behalf_of_email: 'alice@windmill.dev' }
} as any)
expect(await deployDraft('flow', 'f/admin/notify', 'ws')).toEqual({ success: true })
expect(FlowService.updateFlow).toHaveBeenCalledWith(
expect.objectContaining({
requestBody: expect.objectContaining({
on_behalf_of_email: 'alice@windmill.dev',
preserve_on_behalf_of: true
})
})
)
})
})