Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 87 additions & 7 deletions apps/sim/executor/handlers/workflow/workflow-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import {
import type { ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'

const { mockExecutorExecute, mockCreateSnapshot, mockResolveBillingAttribution, executorOptions } =
vi.hoisted(() => ({
mockExecutorExecute: vi.fn(),
mockCreateSnapshot: vi.fn(),
mockResolveBillingAttribution: vi.fn(),
executorOptions: [] as Array<Record<string, any>>,
}))
const {
mockExecutorExecute,
mockCreateSnapshot,
mockResolveBillingAttribution,
mockGetCustomBlockAuthority,
mockGetPersonalAndWorkspaceEnv,
executorOptions,
} = vi.hoisted(() => ({
mockExecutorExecute: vi.fn(),
mockCreateSnapshot: vi.fn(),
mockResolveBillingAttribution: vi.fn(),
mockGetCustomBlockAuthority: vi.fn(),
mockGetPersonalAndWorkspaceEnv: vi.fn(),
executorOptions: [] as Array<Record<string, any>>,
}))

vi.mock('@/executor', () => ({
Executor: class {
Expand All @@ -30,6 +38,14 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({
resolveBillingAttribution: mockResolveBillingAttribution,
}))

vi.mock('@/lib/environment/utils', () => ({
getPersonalAndWorkspaceEnv: mockGetPersonalAndWorkspaceEnv,
}))

vi.mock('@/lib/workflows/custom-blocks/operations', () => ({
getCustomBlockAuthority: mockGetCustomBlockAuthority,
}))

vi.mock('@/lib/logs/execution/snapshot/service', () => ({
snapshotService: { createSnapshotWithDeduplication: mockCreateSnapshot },
}))
Expand Down Expand Up @@ -321,6 +337,70 @@ describe('WorkflowBlockHandler', () => {
expect(mockResolveBillingAttribution).not.toHaveBeenCalled()
})

it('resolves a source-scoped billing attribution for custom block children', async () => {
const consumerAttribution = { actorUserId: 'consumer-1', workspaceId: 'workspace-consumer' }
const sourceAttribution = { actorUserId: 'owner-9', workspaceId: 'workspace-source' }
const customBlock = {
...mockBlock,
metadata: { id: 'custom_block_abc', name: 'Published Block' },
}
const ctx = {
...mockContext,
workspaceId: 'workspace-consumer',
metadata: { ...mockContext.metadata, billingAttribution: consumerAttribution },
} as unknown as ExecutionContext

mockGetCustomBlockAuthority.mockResolvedValue({
workflowId: 'source-workflow-id',
organizationId: 'org-1',
ownerUserId: 'owner-9',
exposedOutputs: [],
requiredInputIds: [],
})
mockGetPersonalAndWorkspaceEnv.mockResolvedValue({
personalDecrypted: {},
workspaceDecrypted: {},
})
mockResolveBillingAttribution.mockResolvedValue(sourceAttribution)
mockFetch.mockImplementation(async (url: unknown) => {
if (String(url).includes('/deployed')) {
return {
ok: true,
json: () =>
Promise.resolve({
data: {
deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} },
},
}),
}
}
return {
ok: true,
json: () =>
Promise.resolve({
data: {
name: 'Source Workflow',
workspaceId: 'workspace-source',
variables: {},
},
}),
}
})
mockCreateSnapshot.mockResolvedValue({ snapshot: { id: 'snapshot-1' } })
mockExecutorExecute.mockResolvedValue({ success: true, output: { data: 'ok' } })

await handler.execute(ctx, customBlock, {})

expect(mockResolveBillingAttribution).toHaveBeenCalledWith({
actorUserId: 'owner-9',
workspaceId: 'workspace-source',
})
expect(executorOptions).toHaveLength(1)
expect(executorOptions[0].contextExtensions.billingAttribution).toBe(sourceAttribution)
expect(executorOptions[0].contextExtensions.userId).toBe('owner-9')
expect(executorOptions[0].contextExtensions.workspaceId).toBe('workspace-source')
})

it('should fail closed when the executing context has no workspace', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
Expand Down
14 changes: 13 additions & 1 deletion apps/sim/lib/admin/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,21 @@ function buildDashboardOrganizationSummary({

export async function listDashboardUsers({ search, limit, offset }: PaginationInput) {
const trimmed = search.trim()
const where = trimmed
// Mirror Better Auth's active-ban semantics: permanent bans and temporary
// bans whose expiry is still in the future stay out of the Users dashboard,
// while an expired temporary ban is treated as lifted. Keep this predicate
// in the database query so pagination totals cannot leak or count hidden rows.
const visibleUser = sql<boolean>`NOT (
coalesce(${user.banned}, false)
AND (
${user.banExpires} IS NULL
OR ${user.banExpires} > (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
)
)`
Comment thread
icecrasher321 marked this conversation as resolved.
const searchMatch = trimmed
? or(ilike(user.name, `%${trimmed}%`), ilike(user.email, `%${trimmed}%`), eq(user.id, trimmed))
: undefined
const where = searchMatch ? and(visibleUser, searchMatch) : visibleUser
const [totalRow, rows] = await Promise.all([
db.select({ total: count() }).from(user).where(where),
db
Expand Down
Loading