Skip to content

Commit f7e498f

Browse files
fix(credentials): preserve entitlement failure reasons
1 parent cea2312 commit f7e498f

4 files changed

Lines changed: 86 additions & 20 deletions

File tree

apps/sim/lib/credential-groups/application/context.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
33
import type { CredentialGroupApplicationContext } from '@/lib/credential-groups/application/authorization'
44
import {
55
isCredentialGroupsAvailable,
6-
isCredentialGroupsEnterprisePlanRequired,
6+
resolveCredentialGroupsAvailability,
77
} from '@/lib/credential-groups/availability'
88
import { loadCredentialGroupCredentialListContext } from '@/lib/credential-groups/credentials'
99
import { loadActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/workspace-context'
1010

1111
export async function requireCredentialGroupsAvailable(workspaceId: string): Promise<void> {
1212
const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(workspaceId)
13-
if (!(await isCredentialGroupsAvailable(ownerBilling))) {
14-
const message = isCredentialGroupsEnterprisePlanRequired(ownerBilling)
15-
? 'Credential Groups are not available. Enterprise plan required.'
16-
: 'Credential Groups are not available'
13+
const availability = await resolveCredentialGroupsAvailability(ownerBilling)
14+
if (!availability.available) {
15+
const message =
16+
availability.reason === 'enterprise_plan_required'
17+
? 'Credential Groups are not available. Enterprise plan required.'
18+
: 'Credential Groups are not available'
1719
throw new OrchestrationError('forbidden', message)
1820
}
1921
}

apps/sim/lib/credential-groups/application/list-credentials.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const mocks = vi.hoisted(() => ({
88
getWorkspaceOwnerSubscriptionAccess: vi.fn(),
9-
isCredentialGroupsAvailable: vi.fn(),
10-
isCredentialGroupsEnterprisePlanRequired: vi.fn(),
119
listCredentials: vi.fn(),
1210
loadGroup: vi.fn(),
1311
loadWorkspace: vi.fn(),
12+
resolveCredentialGroupsAvailability: vi.fn(),
1413
resolvePermission: vi.fn(),
1514
}))
1615

@@ -19,8 +18,7 @@ vi.mock('@/lib/billing/core/workspace-access', () => ({
1918
}))
2019

2120
vi.mock('@/lib/credential-groups/availability', () => ({
22-
isCredentialGroupsAvailable: mocks.isCredentialGroupsAvailable,
23-
isCredentialGroupsEnterprisePlanRequired: mocks.isCredentialGroupsEnterprisePlanRequired,
21+
resolveCredentialGroupsAvailability: mocks.resolveCredentialGroupsAvailability,
2422
}))
2523

2624
vi.mock('@/lib/credential-groups/credentials', () => ({
@@ -106,8 +104,7 @@ describe('listCredentialGroupCredentials', () => {
106104
mocks.loadWorkspace.mockResolvedValue(workspaceContext)
107105
mocks.resolvePermission.mockResolvedValue('read')
108106
mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: true })
109-
mocks.isCredentialGroupsAvailable.mockResolvedValue(true)
110-
mocks.isCredentialGroupsEnterprisePlanRequired.mockReturnValue(false)
107+
mocks.resolveCredentialGroupsAvailability.mockResolvedValue({ available: true })
111108
mocks.listCredentials.mockResolvedValue({
112109
credentials: [
113110
{
@@ -225,18 +222,26 @@ describe('listCredentialGroupCredentials', () => {
225222
})
226223

227224
it('fails before listing when Credential Groups are unavailable', async () => {
228-
mocks.isCredentialGroupsAvailable.mockResolvedValue(false)
225+
mocks.resolveCredentialGroupsAvailability.mockResolvedValue({
226+
available: false,
227+
reason: 'feature_disabled',
228+
})
229229

230230
await expect(
231231
listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
232-
).rejects.toMatchObject({ code: 'forbidden' })
232+
).rejects.toMatchObject({
233+
code: 'forbidden',
234+
message: 'Credential Groups are not available',
235+
})
233236
expect(mocks.listCredentials).not.toHaveBeenCalled()
234237
})
235238

236239
it('identifies the Enterprise requirement for unavailable hosted workspaces', async () => {
237240
mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: false })
238-
mocks.isCredentialGroupsAvailable.mockResolvedValue(false)
239-
mocks.isCredentialGroupsEnterprisePlanRequired.mockReturnValue(true)
241+
mocks.resolveCredentialGroupsAvailability.mockResolvedValue({
242+
available: false,
243+
reason: 'enterprise_plan_required',
244+
})
240245

241246
await expect(
242247
listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const { mockIsFeatureEnabled } = vi.hoisted(() => ({
7+
mockIsFeatureEnabled: vi.fn(),
8+
}))
9+
10+
vi.mock('@/lib/core/config/env-flags', () => ({
11+
isHosted: true,
12+
}))
13+
14+
vi.mock('@/lib/core/config/feature-flags', () => ({
15+
isFeatureEnabled: mockIsFeatureEnabled,
16+
}))
17+
18+
import { resolveCredentialGroupsAvailability } from '@/lib/credential-groups/availability'
19+
20+
describe('resolveCredentialGroupsAvailability', () => {
21+
beforeEach(() => {
22+
vi.clearAllMocks()
23+
})
24+
25+
it('attributes a disabled feature flag before considering the plan', async () => {
26+
mockIsFeatureEnabled.mockResolvedValue(false)
27+
28+
await expect(resolveCredentialGroupsAvailability({ isEnterprise: false })).resolves.toEqual({
29+
available: false,
30+
reason: 'feature_disabled',
31+
})
32+
})
33+
34+
it('requires Enterprise when the hosted feature is enabled', async () => {
35+
mockIsFeatureEnabled.mockResolvedValue(true)
36+
37+
await expect(resolveCredentialGroupsAvailability({ isEnterprise: false })).resolves.toEqual({
38+
available: false,
39+
reason: 'enterprise_plan_required',
40+
})
41+
})
42+
43+
it('allows Enterprise workspaces when the hosted feature is enabled', async () => {
44+
mockIsFeatureEnabled.mockResolvedValue(true)
45+
46+
await expect(resolveCredentialGroupsAvailability({ isEnterprise: true })).resolves.toEqual({
47+
available: true,
48+
})
49+
})
50+
})
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import { isHosted } from '@/lib/core/config/env-flags'
22
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
33

4-
export function isCredentialGroupsEnterprisePlanRequired(ownerBilling: {
4+
export type CredentialGroupsAvailability =
5+
| { available: true }
6+
| { available: false; reason: 'feature_disabled' | 'enterprise_plan_required' }
7+
8+
export async function resolveCredentialGroupsAvailability(ownerBilling: {
59
isEnterprise: boolean
6-
}): boolean {
7-
return isHosted && !ownerBilling.isEnterprise
10+
}): Promise<CredentialGroupsAvailability> {
11+
if (!(await isFeatureEnabled('credential-groups'))) {
12+
return { available: false, reason: 'feature_disabled' }
13+
}
14+
if (isHosted && !ownerBilling.isEnterprise) {
15+
return { available: false, reason: 'enterprise_plan_required' }
16+
}
17+
return { available: true }
818
}
919

1020
/** Credential Groups are globally gated and restricted to Enterprise workspaces on Sim Cloud. */
1121
export async function isCredentialGroupsAvailable(ownerBilling: {
1222
isEnterprise: boolean
1323
}): Promise<boolean> {
14-
if (!(await isFeatureEnabled('credential-groups'))) return false
15-
return !isCredentialGroupsEnterprisePlanRequired(ownerBilling)
24+
return (await resolveCredentialGroupsAvailability(ownerBilling)).available
1625
}

0 commit comments

Comments
 (0)