-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathbyok.test.ts
More file actions
164 lines (133 loc) · 5 KB
/
Copy pathbyok.test.ts
File metadata and controls
164 lines (133 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockOrderBy, mockGetWorkspaceById, mockDecryptSecret } = vi.hoisted(() => ({
mockOrderBy: vi.fn(),
mockGetWorkspaceById: vi.fn(),
mockDecryptSecret: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ orderBy: mockOrderBy })),
})),
})),
},
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getWorkspaceById: mockGetWorkspaceById,
}))
vi.mock('@/lib/core/security/encryption', () => ({
decryptSecret: mockDecryptSecret,
}))
vi.mock('@/lib/core/config/api-keys', () => ({
getRotatingApiKey: vi.fn(),
}))
vi.mock('@/lib/core/config/env', () => ({
env: {},
}))
vi.mock('@/lib/core/config/env-flags', () => ({
isHosted: false,
}))
vi.mock('@/providers/models', () => ({
getProviderFileAttachment: vi
.fn()
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
INLINE_ATTACHMENT_MAX_BYTES: 10 * 1024 * 1024,
getHostedModels: vi.fn(() => []),
}))
vi.mock('@/providers/utils', () => ({
PROVIDER_PLACEHOLDER_KEY: 'placeholder',
}))
vi.mock('@/stores/providers/store', () => ({
useProvidersStore: { getState: vi.fn() },
}))
import { getBYOKKey } from '@/lib/api-key/byok'
/**
* Rotation counters in the module under test are keyed by
* `${workspaceId}:${providerId}` and persist for the process lifetime, so
* each test uses a unique workspace id to start from a fresh cursor.
*/
let testIndex = 0
const uniqueWorkspaceId = () => `workspace-${++testIndex}`
const storedKey = (id: string) => ({ id, encryptedApiKey: `encrypted-${id}` })
describe('getBYOKKey', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetWorkspaceById.mockResolvedValue({ id: 'workspace' })
mockOrderBy.mockResolvedValue([])
mockDecryptSecret.mockImplementation(async (encrypted: string) => ({
decrypted: encrypted.replace('encrypted-', 'decrypted-'),
}))
})
it('returns null when no workspaceId is provided', async () => {
expect(await getBYOKKey(undefined, 'openai')).toBeNull()
expect(await getBYOKKey(null, 'openai')).toBeNull()
expect(mockGetWorkspaceById).not.toHaveBeenCalled()
})
it('returns null when the workspace does not exist', async () => {
mockGetWorkspaceById.mockResolvedValue(null)
expect(await getBYOKKey(uniqueWorkspaceId(), 'openai')).toBeNull()
})
it('returns null when the workspace has no keys for the provider', async () => {
expect(await getBYOKKey(uniqueWorkspaceId(), 'openai')).toBeNull()
})
it('returns the same key on every call when only one key is stored', async () => {
const workspaceId = uniqueWorkspaceId()
mockOrderBy.mockResolvedValue([storedKey('key-1')])
for (let call = 0; call < 3; call++) {
expect(await getBYOKKey(workspaceId, 'openai')).toEqual({
apiKey: 'decrypted-key-1',
isBYOK: true,
})
}
})
it('round-robins across multiple keys in creation order', async () => {
const workspaceId = uniqueWorkspaceId()
mockOrderBy.mockResolvedValue([storedKey('key-1'), storedKey('key-2'), storedKey('key-3')])
const apiKeys = []
for (let call = 0; call < 4; call++) {
const result = await getBYOKKey(workspaceId, 'openai')
apiKeys.push(result?.apiKey)
}
expect(apiKeys).toEqual([
'decrypted-key-1',
'decrypted-key-2',
'decrypted-key-3',
'decrypted-key-1',
])
})
it('tracks rotation independently per provider within a workspace', async () => {
const workspaceId = uniqueWorkspaceId()
mockOrderBy.mockResolvedValue([storedKey('key-1'), storedKey('key-2')])
expect((await getBYOKKey(workspaceId, 'openai'))?.apiKey).toBe('decrypted-key-1')
expect((await getBYOKKey(workspaceId, 'anthropic'))?.apiKey).toBe('decrypted-key-1')
expect((await getBYOKKey(workspaceId, 'openai'))?.apiKey).toBe('decrypted-key-2')
})
it('skips a key that fails to decrypt and returns the next one', async () => {
const workspaceId = uniqueWorkspaceId()
mockOrderBy.mockResolvedValue([storedKey('key-1'), storedKey('key-2')])
mockDecryptSecret.mockImplementation(async (encrypted: string) => {
if (encrypted === 'encrypted-key-1') {
throw new Error('corrupt ciphertext')
}
return { decrypted: encrypted.replace('encrypted-', 'decrypted-') }
})
expect(await getBYOKKey(workspaceId, 'openai')).toEqual({
apiKey: 'decrypted-key-2',
isBYOK: true,
})
})
it('returns null when every key fails to decrypt', async () => {
const workspaceId = uniqueWorkspaceId()
mockOrderBy.mockResolvedValue([storedKey('key-1'), storedKey('key-2')])
mockDecryptSecret.mockRejectedValue(new Error('corrupt ciphertext'))
expect(await getBYOKKey(workspaceId, 'openai')).toBeNull()
})
it('returns null when the keys query throws', async () => {
mockOrderBy.mockRejectedValue(new Error('database unavailable'))
expect(await getBYOKKey(uniqueWorkspaceId(), 'openai')).toBeNull()
})
})