Skip to content

Commit 53e3d19

Browse files
committed
tests
1 parent 7485776 commit 53e3d19

3 files changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { createMockRequest, hybridAuthMockFns } from '@sim/testing'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const {
8+
mockExecuteProviderRequest,
9+
mockRequireBillingAttributionHeader,
10+
mockCheckWorkspaceAccess,
11+
mockAuthorizeCredentialUse,
12+
} = vi.hoisted(() => ({
13+
mockExecuteProviderRequest: vi.fn(),
14+
mockRequireBillingAttributionHeader: vi.fn(),
15+
mockCheckWorkspaceAccess: vi.fn(),
16+
mockAuthorizeCredentialUse: vi.fn(),
17+
}))
18+
19+
vi.mock('@/providers', () => ({
20+
executeProviderRequest: mockExecuteProviderRequest,
21+
}))
22+
23+
vi.mock('@/lib/billing/core/billing-attribution', () => ({
24+
BILLING_ATTRIBUTION_HEADER: 'x-sim-billing-attribution',
25+
requireBillingAttributionHeader: mockRequireBillingAttributionHeader,
26+
}))
27+
28+
vi.mock('@/lib/workspaces/permissions/utils', () => ({
29+
checkWorkspaceAccess: mockCheckWorkspaceAccess,
30+
}))
31+
32+
vi.mock('@/lib/auth/credential-access', () => ({
33+
authorizeCredentialUse: mockAuthorizeCredentialUse,
34+
}))
35+
36+
vi.mock('@/app/api/auth/oauth/utils', () => ({
37+
getServiceAccountToken: vi.fn(),
38+
refreshTokenIfNeeded: vi.fn(),
39+
resolveOAuthAccountId: vi.fn(),
40+
}))
41+
42+
vi.mock('@/ee/access-control/utils/permission-check', () => ({
43+
assertPermissionsAllowed: vi.fn(),
44+
IntegrationNotAllowedError: class IntegrationNotAllowedError extends Error {},
45+
ModelNotAllowedError: class ModelNotAllowedError extends Error {},
46+
ProviderNotAllowedError: class ProviderNotAllowedError extends Error {},
47+
}))
48+
49+
import { POST } from '@/app/api/providers/route'
50+
51+
const BILLING_ATTRIBUTION = {
52+
actorUserId: 'user-1',
53+
workspaceId: 'ws-1',
54+
organizationId: 'org-1',
55+
billedAccountUserId: 'owner-1',
56+
billingEntity: { type: 'organization', id: 'org-1' },
57+
billingPeriod: {
58+
start: '2026-07-01T00:00:00.000Z',
59+
end: '2026-08-01T00:00:00.000Z',
60+
},
61+
payerSubscription: null,
62+
}
63+
64+
describe('POST /api/providers', () => {
65+
beforeEach(() => {
66+
vi.clearAllMocks()
67+
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
68+
success: true,
69+
userId: 'user-1',
70+
authType: 'internal_jwt',
71+
})
72+
mockCheckWorkspaceAccess.mockResolvedValue({ hasAccess: true })
73+
mockRequireBillingAttributionHeader.mockReturnValue(BILLING_ATTRIBUTION)
74+
mockExecuteProviderRequest.mockResolvedValue({
75+
content: 'hello',
76+
model: 'gpt-4o',
77+
tokens: { input: 1, output: 1, total: 2 },
78+
})
79+
})
80+
81+
it('validates the attribution header and forwards it to executeProviderRequest', async () => {
82+
const res = await POST(
83+
createMockRequest(
84+
'POST',
85+
{ provider: 'openai', model: 'gpt-4o', workspaceId: 'ws-1' },
86+
{ 'x-sim-billing-attribution': 'encoded-attribution' }
87+
)
88+
)
89+
90+
expect(res.status).toBe(200)
91+
expect(mockRequireBillingAttributionHeader).toHaveBeenCalledWith(expect.anything(), {
92+
actorUserId: 'user-1',
93+
workspaceId: 'ws-1',
94+
})
95+
expect(mockExecuteProviderRequest).toHaveBeenCalledWith(
96+
'openai',
97+
expect.objectContaining({ billingAttribution: BILLING_ATTRIBUTION })
98+
)
99+
})
100+
101+
it('executes without attribution when the header is absent', async () => {
102+
const res = await POST(
103+
createMockRequest('POST', { provider: 'openai', model: 'gpt-4o', workspaceId: 'ws-1' })
104+
)
105+
106+
expect(res.status).toBe(200)
107+
expect(mockRequireBillingAttributionHeader).not.toHaveBeenCalled()
108+
expect(mockExecuteProviderRequest).toHaveBeenCalledWith(
109+
'openai',
110+
expect.objectContaining({ billingAttribution: undefined })
111+
)
112+
})
113+
114+
it('ignores the attribution header when the body has no workspaceId to validate against', async () => {
115+
const res = await POST(
116+
createMockRequest(
117+
'POST',
118+
{ provider: 'openai', model: 'gpt-4o' },
119+
{ 'x-sim-billing-attribution': 'encoded-attribution' }
120+
)
121+
)
122+
123+
expect(res.status).toBe(200)
124+
expect(mockRequireBillingAttributionHeader).not.toHaveBeenCalled()
125+
expect(mockExecuteProviderRequest).toHaveBeenCalledWith(
126+
'openai',
127+
expect.objectContaining({ billingAttribution: undefined })
128+
)
129+
})
130+
131+
it('fails when the attribution header does not match the authenticated scope', async () => {
132+
mockRequireBillingAttributionHeader.mockImplementation(() => {
133+
throw new Error('Billing attribution header does not match the authenticated request scope')
134+
})
135+
136+
const res = await POST(
137+
createMockRequest(
138+
'POST',
139+
{ provider: 'openai', model: 'gpt-4o', workspaceId: 'ws-1' },
140+
{ 'x-sim-billing-attribution': 'encoded-attribution' }
141+
)
142+
)
143+
144+
expect(res.status).toBe(500)
145+
expect(mockExecuteProviderRequest).not.toHaveBeenCalled()
146+
})
147+
})

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,6 +1828,42 @@ describe('AgentBlockHandler', () => {
18281828
expect(providerCallArgs.callChain).toEqual(['wf-parent', 'test-workflow-456'])
18291829
})
18301830

1831+
it('should pass billingAttribution to executeProviderRequest so LLM tool calls carry it', async () => {
1832+
const billingAttribution = {
1833+
actorUserId: 'user-1',
1834+
workspaceId: 'test-workspace-123',
1835+
organizationId: 'organization-1',
1836+
billedAccountUserId: 'owner-1',
1837+
billingEntity: { type: 'organization', id: 'organization-1' },
1838+
billingPeriod: {
1839+
start: '2026-07-01T00:00:00.000Z',
1840+
end: '2026-08-01T00:00:00.000Z',
1841+
},
1842+
payerSubscription: null,
1843+
}
1844+
1845+
const inputs = {
1846+
model: 'gpt-4o',
1847+
userPrompt: 'Search the knowledge base',
1848+
apiKey: 'test-api-key',
1849+
}
1850+
1851+
const contextWithAttribution = {
1852+
...mockContext,
1853+
workspaceId: 'test-workspace-123',
1854+
workflowId: 'test-workflow-456',
1855+
metadata: { ...mockContext.metadata, billingAttribution },
1856+
} as ExecutionContext
1857+
1858+
mockGetProviderFromModel.mockReturnValue('openai')
1859+
1860+
await handler.execute(contextWithAttribution, mockBlock, inputs)
1861+
1862+
expect(mockExecuteProviderRequest).toHaveBeenCalled()
1863+
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0][1]
1864+
expect(providerCallArgs.billingAttribution).toEqual(billingAttribution)
1865+
})
1866+
18311867
it('should handle multiple MCP tools from the same server efficiently', async () => {
18321868
const fetchCalls: any[] = []
18331869

apps/sim/providers/utils.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,45 @@ describe('prepareToolExecution', () => {
12901290
})
12911291
})
12921292

1293+
describe('_context propagation', () => {
1294+
const billingAttribution = {
1295+
actorUserId: 'user-1',
1296+
workspaceId: 'workspace-1',
1297+
organizationId: 'organization-1',
1298+
billedAccountUserId: 'owner-1',
1299+
billingEntity: { type: 'organization' as const, id: 'organization-1' },
1300+
billingPeriod: {
1301+
start: '2026-07-01T00:00:00.000Z',
1302+
end: '2026-08-01T00:00:00.000Z',
1303+
},
1304+
payerSubscription: null,
1305+
}
1306+
1307+
it.concurrent('should include billingAttribution in _context when the request carries it', () => {
1308+
const tool = { params: {} }
1309+
const request = {
1310+
workflowId: 'wf-123',
1311+
workspaceId: 'workspace-1',
1312+
userId: 'user-1',
1313+
billingAttribution,
1314+
}
1315+
1316+
const { executionParams } = prepareToolExecution(tool, {}, request)
1317+
1318+
expect(executionParams._context.billingAttribution).toEqual(billingAttribution)
1319+
})
1320+
1321+
it.concurrent('should omit billingAttribution from _context when the request lacks it', () => {
1322+
const tool = { params: {} }
1323+
const request = { workflowId: 'wf-123', workspaceId: 'workspace-1' }
1324+
1325+
const { executionParams } = prepareToolExecution(tool, {}, request)
1326+
1327+
expect(executionParams._context).toBeDefined()
1328+
expect(executionParams._context).not.toHaveProperty('billingAttribution')
1329+
})
1330+
})
1331+
12931332
describe('inputMapping deep merge for workflow tools', () => {
12941333
it.concurrent('should deep merge inputMapping when user provides empty object', () => {
12951334
const tool = {

0 commit comments

Comments
 (0)