@mdxui/terminal 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal SaaS Collections Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for SaaS primitive collections
|
|
5
|
+
* that power the Developer Dashboard and SaaS app features.
|
|
6
|
+
*
|
|
7
|
+
* Collections tested:
|
|
8
|
+
* - Users: Basic user account information (id, name, email, role, createdAt)
|
|
9
|
+
* - APIKeys: API key management (id, key, name, permissions, expiresAt)
|
|
10
|
+
* - Webhooks: Webhook configuration (id, url, events, secret, active)
|
|
11
|
+
* - Teams: Team management (id, name, members)
|
|
12
|
+
* - Usage: Usage metrics tracking (id, metric, value, timestamp)
|
|
13
|
+
*
|
|
14
|
+
* NOTE: These tests are expected to FAIL until collections are implemented.
|
|
15
|
+
* Run: pnpm --filter @mdxui/terminal test -- --run src/__tests__/data/collections.test.ts
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// These imports WILL FAIL until src/data/saas-collections.ts is implemented
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
UsersCollection,
|
|
27
|
+
APIKeysCollection,
|
|
28
|
+
WebhooksCollection,
|
|
29
|
+
TeamsCollection,
|
|
30
|
+
UsageCollection,
|
|
31
|
+
} from '../../data/saas-collections'
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
UserSchema,
|
|
35
|
+
APIKeySchema,
|
|
36
|
+
WebhookSchema,
|
|
37
|
+
TeamSchema,
|
|
38
|
+
UsageSchema,
|
|
39
|
+
type User,
|
|
40
|
+
type APIKey,
|
|
41
|
+
type Webhook,
|
|
42
|
+
type Team,
|
|
43
|
+
type Usage,
|
|
44
|
+
} from '../../data/saas-collections'
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Users Collection Tests
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
describe('Users Collection', () => {
|
|
51
|
+
let usersCollection: ReturnType<typeof UsersCollection>
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
usersCollection = UsersCollection()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('schema validation', () => {
|
|
58
|
+
it('validates a valid user object', () => {
|
|
59
|
+
const validUser: User = {
|
|
60
|
+
id: 'user-1',
|
|
61
|
+
name: 'Alice Johnson',
|
|
62
|
+
email: 'alice@example.com',
|
|
63
|
+
role: 'admin',
|
|
64
|
+
createdAt: new Date('2024-01-15'),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = UserSchema.safeParse(validUser)
|
|
68
|
+
expect(result.success).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('requires id field', () => {
|
|
72
|
+
const invalidUser = {
|
|
73
|
+
name: 'Alice Johnson',
|
|
74
|
+
email: 'alice@example.com',
|
|
75
|
+
role: 'admin',
|
|
76
|
+
createdAt: new Date(),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
80
|
+
expect(result.success).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('requires name field', () => {
|
|
84
|
+
const invalidUser = {
|
|
85
|
+
id: 'user-1',
|
|
86
|
+
email: 'alice@example.com',
|
|
87
|
+
role: 'admin',
|
|
88
|
+
createdAt: new Date(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
92
|
+
expect(result.success).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('requires valid email format', () => {
|
|
96
|
+
const invalidUser = {
|
|
97
|
+
id: 'user-1',
|
|
98
|
+
name: 'Alice Johnson',
|
|
99
|
+
email: 'not-an-email',
|
|
100
|
+
role: 'admin',
|
|
101
|
+
createdAt: new Date(),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
105
|
+
expect(result.success).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('requires role field as enum (admin, user, viewer)', () => {
|
|
109
|
+
const invalidUser = {
|
|
110
|
+
id: 'user-1',
|
|
111
|
+
name: 'Alice Johnson',
|
|
112
|
+
email: 'alice@example.com',
|
|
113
|
+
role: 'superadmin',
|
|
114
|
+
createdAt: new Date(),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
118
|
+
expect(result.success).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('requires createdAt as Date', () => {
|
|
122
|
+
const invalidUser = {
|
|
123
|
+
id: 'user-1',
|
|
124
|
+
name: 'Alice Johnson',
|
|
125
|
+
email: 'alice@example.com',
|
|
126
|
+
role: 'admin',
|
|
127
|
+
createdAt: '2024-01-15',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
131
|
+
expect(result.success).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('accepts all valid roles: admin, user, viewer', () => {
|
|
135
|
+
const roles = ['admin', 'user', 'viewer'] as const
|
|
136
|
+
|
|
137
|
+
roles.forEach((role) => {
|
|
138
|
+
const user: User = {
|
|
139
|
+
id: `user-${role}`,
|
|
140
|
+
name: 'Test User',
|
|
141
|
+
email: `${role}@example.com`,
|
|
142
|
+
role,
|
|
143
|
+
createdAt: new Date(),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = UserSchema.safeParse(user)
|
|
147
|
+
expect(result.success).toBe(true)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('CRUD operations', () => {
|
|
153
|
+
it('inserts a new user', async () => {
|
|
154
|
+
const newUser: User = {
|
|
155
|
+
id: 'user-1',
|
|
156
|
+
name: 'Alice Johnson',
|
|
157
|
+
email: 'alice@example.com',
|
|
158
|
+
role: 'admin',
|
|
159
|
+
createdAt: new Date('2024-01-15'),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inserted = await usersCollection.insert(newUser)
|
|
163
|
+
expect(inserted.id).toBe('user-1')
|
|
164
|
+
expect(inserted.name).toBe('Alice Johnson')
|
|
165
|
+
expect(inserted.email).toBe('alice@example.com')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('rejects duplicate user id on insert', async () => {
|
|
169
|
+
const user: User = {
|
|
170
|
+
id: 'user-1',
|
|
171
|
+
name: 'Alice Johnson',
|
|
172
|
+
email: 'alice@example.com',
|
|
173
|
+
role: 'admin',
|
|
174
|
+
createdAt: new Date(),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await usersCollection.insert(user)
|
|
178
|
+
|
|
179
|
+
const duplicate: User = {
|
|
180
|
+
id: 'user-1',
|
|
181
|
+
name: 'Different Name',
|
|
182
|
+
email: 'different@example.com',
|
|
183
|
+
role: 'user',
|
|
184
|
+
createdAt: new Date(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await expect(usersCollection.insert(duplicate)).rejects.toThrow()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('finds a single user by id', async () => {
|
|
191
|
+
const user: User = {
|
|
192
|
+
id: 'user-find-1',
|
|
193
|
+
name: 'Bob Smith',
|
|
194
|
+
email: 'bob@example.com',
|
|
195
|
+
role: 'user',
|
|
196
|
+
createdAt: new Date(),
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await usersCollection.insert(user)
|
|
200
|
+
const found = await usersCollection.findOne({ id: 'user-find-1' })
|
|
201
|
+
|
|
202
|
+
expect(found).not.toBeNull()
|
|
203
|
+
expect(found?.name).toBe('Bob Smith')
|
|
204
|
+
expect(found?.email).toBe('bob@example.com')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('finds multiple users with filtering', async () => {
|
|
208
|
+
const users: User[] = [
|
|
209
|
+
{
|
|
210
|
+
id: 'admin-1',
|
|
211
|
+
name: 'Admin User',
|
|
212
|
+
email: 'admin@example.com',
|
|
213
|
+
role: 'admin',
|
|
214
|
+
createdAt: new Date('2024-01-01'),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'user-1',
|
|
218
|
+
name: 'Regular User',
|
|
219
|
+
email: 'user@example.com',
|
|
220
|
+
role: 'user',
|
|
221
|
+
createdAt: new Date('2024-01-15'),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'user-2',
|
|
225
|
+
name: 'Another User',
|
|
226
|
+
email: 'another@example.com',
|
|
227
|
+
role: 'user',
|
|
228
|
+
createdAt: new Date('2024-02-01'),
|
|
229
|
+
},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
for (const user of users) {
|
|
233
|
+
await usersCollection.insert(user)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const allUsers = await usersCollection.findMany()
|
|
237
|
+
expect(allUsers).toHaveLength(3)
|
|
238
|
+
|
|
239
|
+
const adminUsers = await usersCollection.findMany({
|
|
240
|
+
where: { role: 'admin' },
|
|
241
|
+
})
|
|
242
|
+
expect(adminUsers).toHaveLength(1)
|
|
243
|
+
expect(adminUsers[0].name).toBe('Admin User')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('updates user information', async () => {
|
|
247
|
+
const user: User = {
|
|
248
|
+
id: 'user-update-1',
|
|
249
|
+
name: 'Original Name',
|
|
250
|
+
email: 'original@example.com',
|
|
251
|
+
role: 'user',
|
|
252
|
+
createdAt: new Date(),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await usersCollection.insert(user)
|
|
256
|
+
|
|
257
|
+
const updated = await usersCollection.update(
|
|
258
|
+
{ id: 'user-update-1' },
|
|
259
|
+
{ name: 'Updated Name', role: 'admin' }
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
expect(updated).toHaveLength(1)
|
|
263
|
+
expect(updated[0].name).toBe('Updated Name')
|
|
264
|
+
expect(updated[0].role).toBe('admin')
|
|
265
|
+
expect(updated[0].email).toBe('original@example.com')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('deletes a user', async () => {
|
|
269
|
+
const user: User = {
|
|
270
|
+
id: 'user-delete-1',
|
|
271
|
+
name: 'To Delete',
|
|
272
|
+
email: 'delete@example.com',
|
|
273
|
+
role: 'user',
|
|
274
|
+
createdAt: new Date(),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await usersCollection.insert(user)
|
|
278
|
+
await usersCollection.delete({ id: 'user-delete-1' })
|
|
279
|
+
|
|
280
|
+
const found = await usersCollection.findOne({ id: 'user-delete-1' })
|
|
281
|
+
expect(found).toBeNull()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('sorts users by createdAt in descending order', async () => {
|
|
285
|
+
const users: User[] = [
|
|
286
|
+
{
|
|
287
|
+
id: 'user-old',
|
|
288
|
+
name: 'Old User',
|
|
289
|
+
email: 'old@example.com',
|
|
290
|
+
role: 'user',
|
|
291
|
+
createdAt: new Date('2024-01-01'),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'user-new',
|
|
295
|
+
name: 'New User',
|
|
296
|
+
email: 'new@example.com',
|
|
297
|
+
role: 'user',
|
|
298
|
+
createdAt: new Date('2024-02-01'),
|
|
299
|
+
},
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
for (const user of users) {
|
|
303
|
+
await usersCollection.insert(user)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const sorted = await usersCollection.findMany({
|
|
307
|
+
orderBy: { createdAt: 'desc' },
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
expect(sorted[0].name).toBe('New User')
|
|
311
|
+
expect(sorted[1].name).toBe('Old User')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('subscriptions', () => {
|
|
316
|
+
it('subscribes to collection changes', async () => {
|
|
317
|
+
let lastData: User[] | null = null
|
|
318
|
+
|
|
319
|
+
const unsubscribe = usersCollection.subscribe((data) => {
|
|
320
|
+
lastData = data
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const user: User = {
|
|
324
|
+
id: 'user-sub-1',
|
|
325
|
+
name: 'Subscription Test',
|
|
326
|
+
email: 'sub@example.com',
|
|
327
|
+
role: 'user',
|
|
328
|
+
createdAt: new Date(),
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await usersCollection.insert(user)
|
|
332
|
+
|
|
333
|
+
expect(lastData).not.toBeNull()
|
|
334
|
+
expect(lastData).toHaveLength(1)
|
|
335
|
+
expect(lastData![0].id).toBe('user-sub-1')
|
|
336
|
+
|
|
337
|
+
unsubscribe()
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// APIKeys Collection Tests
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
describe('APIKeys Collection', () => {
|
|
347
|
+
let apiKeysCollection: ReturnType<typeof APIKeysCollection>
|
|
348
|
+
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
apiKeysCollection = APIKeysCollection()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe('schema validation', () => {
|
|
354
|
+
it('validates a valid API key object', () => {
|
|
355
|
+
const validAPIKey: APIKey = {
|
|
356
|
+
id: 'key-1',
|
|
357
|
+
key: 'sk_test_123456789abcdef',
|
|
358
|
+
name: 'Development Key',
|
|
359
|
+
permissions: ['read:api', 'write:webhooks'],
|
|
360
|
+
expiresAt: new Date('2025-12-31'),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const result = APIKeySchema.safeParse(validAPIKey)
|
|
364
|
+
expect(result.success).toBe(true)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('requires id field', () => {
|
|
368
|
+
const invalidKey = {
|
|
369
|
+
key: 'sk_test_123456789abcdef',
|
|
370
|
+
name: 'Development Key',
|
|
371
|
+
permissions: ['read:api'],
|
|
372
|
+
expiresAt: new Date(),
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
376
|
+
expect(result.success).toBe(false)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('requires key field (secret key string)', () => {
|
|
380
|
+
const invalidKey = {
|
|
381
|
+
id: 'key-1',
|
|
382
|
+
name: 'Development Key',
|
|
383
|
+
permissions: ['read:api'],
|
|
384
|
+
expiresAt: new Date(),
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
388
|
+
expect(result.success).toBe(false)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('requires name field', () => {
|
|
392
|
+
const invalidKey = {
|
|
393
|
+
id: 'key-1',
|
|
394
|
+
key: 'sk_test_123456789abcdef',
|
|
395
|
+
permissions: ['read:api'],
|
|
396
|
+
expiresAt: new Date(),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
400
|
+
expect(result.success).toBe(false)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('requires permissions as array of strings', () => {
|
|
404
|
+
const invalidKey = {
|
|
405
|
+
id: 'key-1',
|
|
406
|
+
key: 'sk_test_123456789abcdef',
|
|
407
|
+
name: 'Development Key',
|
|
408
|
+
permissions: 'read:api',
|
|
409
|
+
expiresAt: new Date(),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
413
|
+
expect(result.success).toBe(false)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('requires expiresAt as Date', () => {
|
|
417
|
+
const invalidKey = {
|
|
418
|
+
id: 'key-1',
|
|
419
|
+
key: 'sk_test_123456789abcdef',
|
|
420
|
+
name: 'Development Key',
|
|
421
|
+
permissions: ['read:api'],
|
|
422
|
+
expiresAt: '2025-12-31',
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
426
|
+
expect(result.success).toBe(false)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('accepts multiple permissions', () => {
|
|
430
|
+
const apiKey: APIKey = {
|
|
431
|
+
id: 'key-multi',
|
|
432
|
+
key: 'sk_test_abc',
|
|
433
|
+
name: 'Multi-Permission Key',
|
|
434
|
+
permissions: ['read:api', 'write:webhooks', 'admin:settings'],
|
|
435
|
+
expiresAt: new Date(),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = APIKeySchema.safeParse(apiKey)
|
|
439
|
+
expect(result.success).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
describe('CRUD operations', () => {
|
|
444
|
+
it('inserts a new API key', async () => {
|
|
445
|
+
const newKey: APIKey = {
|
|
446
|
+
id: 'key-insert-1',
|
|
447
|
+
key: 'sk_test_123456789abcdef',
|
|
448
|
+
name: 'Development Key',
|
|
449
|
+
permissions: ['read:api', 'write:webhooks'],
|
|
450
|
+
expiresAt: new Date('2025-12-31'),
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const inserted = await apiKeysCollection.insert(newKey)
|
|
454
|
+
expect(inserted.id).toBe('key-insert-1')
|
|
455
|
+
expect(inserted.name).toBe('Development Key')
|
|
456
|
+
expect(inserted.permissions).toHaveLength(2)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('finds API keys by id', async () => {
|
|
460
|
+
const key: APIKey = {
|
|
461
|
+
id: 'key-find-1',
|
|
462
|
+
key: 'sk_test_abc',
|
|
463
|
+
name: 'Find Test',
|
|
464
|
+
permissions: ['read:api'],
|
|
465
|
+
expiresAt: new Date(),
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await apiKeysCollection.insert(key)
|
|
469
|
+
const found = await apiKeysCollection.findOne({ id: 'key-find-1' })
|
|
470
|
+
|
|
471
|
+
expect(found).not.toBeNull()
|
|
472
|
+
expect(found?.name).toBe('Find Test')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('finds multiple API keys with filtering', async () => {
|
|
476
|
+
const keys: APIKey[] = [
|
|
477
|
+
{
|
|
478
|
+
id: 'key-1',
|
|
479
|
+
key: 'sk_test_1',
|
|
480
|
+
name: 'Prod Key',
|
|
481
|
+
permissions: ['read:api', 'write:webhooks', 'admin:settings'],
|
|
482
|
+
expiresAt: new Date('2026-12-31'),
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'key-2',
|
|
486
|
+
key: 'sk_test_2',
|
|
487
|
+
name: 'Dev Key',
|
|
488
|
+
permissions: ['read:api'],
|
|
489
|
+
expiresAt: new Date('2025-12-31'),
|
|
490
|
+
},
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
for (const key of keys) {
|
|
494
|
+
await apiKeysCollection.insert(key)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const allKeys = await apiKeysCollection.findMany()
|
|
498
|
+
expect(allKeys).toHaveLength(2)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('updates API key name and permissions', async () => {
|
|
502
|
+
const key: APIKey = {
|
|
503
|
+
id: 'key-update-1',
|
|
504
|
+
key: 'sk_test_original',
|
|
505
|
+
name: 'Original Name',
|
|
506
|
+
permissions: ['read:api'],
|
|
507
|
+
expiresAt: new Date(),
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await apiKeysCollection.insert(key)
|
|
511
|
+
|
|
512
|
+
const updated = await apiKeysCollection.update(
|
|
513
|
+
{ id: 'key-update-1' },
|
|
514
|
+
{ name: 'Updated Name', permissions: ['read:api', 'write:webhooks'] }
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
expect(updated).toHaveLength(1)
|
|
518
|
+
expect(updated[0].name).toBe('Updated Name')
|
|
519
|
+
expect(updated[0].permissions).toHaveLength(2)
|
|
520
|
+
expect(updated[0].key).toBe('sk_test_original')
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('deletes an API key', async () => {
|
|
524
|
+
const key: APIKey = {
|
|
525
|
+
id: 'key-delete-1',
|
|
526
|
+
key: 'sk_test_delete',
|
|
527
|
+
name: 'To Delete',
|
|
528
|
+
permissions: ['read:api'],
|
|
529
|
+
expiresAt: new Date(),
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await apiKeysCollection.insert(key)
|
|
533
|
+
await apiKeysCollection.delete({ id: 'key-delete-1' })
|
|
534
|
+
|
|
535
|
+
const found = await apiKeysCollection.findOne({ id: 'key-delete-1' })
|
|
536
|
+
expect(found).toBeNull()
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('filters API keys by expiration', async () => {
|
|
540
|
+
const now = new Date()
|
|
541
|
+
const expired = new Date(now.getTime() - 86400000)
|
|
542
|
+
const future = new Date(now.getTime() + 86400000)
|
|
543
|
+
|
|
544
|
+
const keys: APIKey[] = [
|
|
545
|
+
{
|
|
546
|
+
id: 'key-expired',
|
|
547
|
+
key: 'sk_expired',
|
|
548
|
+
name: 'Expired Key',
|
|
549
|
+
permissions: [],
|
|
550
|
+
expiresAt: expired,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: 'key-valid',
|
|
554
|
+
key: 'sk_valid',
|
|
555
|
+
name: 'Valid Key',
|
|
556
|
+
permissions: [],
|
|
557
|
+
expiresAt: future,
|
|
558
|
+
},
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
for (const key of keys) {
|
|
562
|
+
await apiKeysCollection.insert(key)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const validKeys = await apiKeysCollection.findMany({
|
|
566
|
+
where: { expiresAt: { $gt: now } },
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
expect(validKeys).toHaveLength(1)
|
|
570
|
+
expect(validKeys[0].name).toBe('Valid Key')
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
// ============================================================================
|
|
576
|
+
// Webhooks Collection Tests
|
|
577
|
+
// ============================================================================
|
|
578
|
+
|
|
579
|
+
describe('Webhooks Collection', () => {
|
|
580
|
+
let webhooksCollection: ReturnType<typeof WebhooksCollection>
|
|
581
|
+
|
|
582
|
+
beforeEach(() => {
|
|
583
|
+
webhooksCollection = WebhooksCollection()
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
describe('schema validation', () => {
|
|
587
|
+
it('validates a valid webhook object', () => {
|
|
588
|
+
const validWebhook: Webhook = {
|
|
589
|
+
id: 'webhook-1',
|
|
590
|
+
url: 'https://example.com/webhook',
|
|
591
|
+
events: ['user.created', 'user.updated'],
|
|
592
|
+
secret: 'whsec_test_123456789abcdef',
|
|
593
|
+
active: true,
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const result = WebhookSchema.safeParse(validWebhook)
|
|
597
|
+
expect(result.success).toBe(true)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('requires valid URL format', () => {
|
|
601
|
+
const invalidWebhook = {
|
|
602
|
+
id: 'webhook-1',
|
|
603
|
+
url: 'not-a-url',
|
|
604
|
+
events: ['user.created'],
|
|
605
|
+
secret: 'whsec_test_123456789abcdef',
|
|
606
|
+
active: true,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const result = WebhookSchema.safeParse(invalidWebhook)
|
|
610
|
+
expect(result.success).toBe(false)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('requires events as non-empty array', () => {
|
|
614
|
+
const invalidWebhook = {
|
|
615
|
+
id: 'webhook-1',
|
|
616
|
+
url: 'https://example.com/webhook',
|
|
617
|
+
events: [],
|
|
618
|
+
secret: 'whsec_test_123456789abcdef',
|
|
619
|
+
active: true,
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const result = WebhookSchema.safeParse(invalidWebhook)
|
|
623
|
+
expect(result.success).toBe(false)
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('requires secret field', () => {
|
|
627
|
+
const invalidWebhook = {
|
|
628
|
+
id: 'webhook-1',
|
|
629
|
+
url: 'https://example.com/webhook',
|
|
630
|
+
events: ['user.created'],
|
|
631
|
+
active: true,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const result = WebhookSchema.safeParse(invalidWebhook)
|
|
635
|
+
expect(result.success).toBe(false)
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
it('requires active as boolean', () => {
|
|
639
|
+
const invalidWebhook = {
|
|
640
|
+
id: 'webhook-1',
|
|
641
|
+
url: 'https://example.com/webhook',
|
|
642
|
+
events: ['user.created'],
|
|
643
|
+
secret: 'whsec_test_123456789abcdef',
|
|
644
|
+
active: 'yes',
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const result = WebhookSchema.safeParse(invalidWebhook)
|
|
648
|
+
expect(result.success).toBe(false)
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
describe('CRUD operations', () => {
|
|
653
|
+
it('inserts a new webhook', async () => {
|
|
654
|
+
const newWebhook: Webhook = {
|
|
655
|
+
id: 'webhook-insert-1',
|
|
656
|
+
url: 'https://example.com/webhook',
|
|
657
|
+
events: ['user.created', 'user.updated'],
|
|
658
|
+
secret: 'whsec_test_123456789abcdef',
|
|
659
|
+
active: true,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const inserted = await webhooksCollection.insert(newWebhook)
|
|
663
|
+
expect(inserted.id).toBe('webhook-insert-1')
|
|
664
|
+
expect(inserted.url).toBe('https://example.com/webhook')
|
|
665
|
+
expect(inserted.events).toHaveLength(2)
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('finds webhooks by id', async () => {
|
|
669
|
+
const webhook: Webhook = {
|
|
670
|
+
id: 'webhook-find-1',
|
|
671
|
+
url: 'https://api.example.com/webhook',
|
|
672
|
+
events: ['api.call'],
|
|
673
|
+
secret: 'whsec_test_abc',
|
|
674
|
+
active: true,
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
await webhooksCollection.insert(webhook)
|
|
678
|
+
const found = await webhooksCollection.findOne({ id: 'webhook-find-1' })
|
|
679
|
+
|
|
680
|
+
expect(found).not.toBeNull()
|
|
681
|
+
expect(found?.url).toBe('https://api.example.com/webhook')
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
it('filters active webhooks', async () => {
|
|
685
|
+
const webhooks: Webhook[] = [
|
|
686
|
+
{
|
|
687
|
+
id: 'webhook-active',
|
|
688
|
+
url: 'https://active.example.com/webhook',
|
|
689
|
+
events: ['test'],
|
|
690
|
+
secret: 'whsec_active',
|
|
691
|
+
active: true,
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
id: 'webhook-inactive',
|
|
695
|
+
url: 'https://inactive.example.com/webhook',
|
|
696
|
+
events: ['test'],
|
|
697
|
+
secret: 'whsec_inactive',
|
|
698
|
+
active: false,
|
|
699
|
+
},
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
for (const webhook of webhooks) {
|
|
703
|
+
await webhooksCollection.insert(webhook)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const activeWebhooks = await webhooksCollection.findMany({
|
|
707
|
+
where: { active: true },
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
expect(activeWebhooks).toHaveLength(1)
|
|
711
|
+
expect(activeWebhooks[0].id).toBe('webhook-active')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('updates webhook url and events', async () => {
|
|
715
|
+
const webhook: Webhook = {
|
|
716
|
+
id: 'webhook-update-1',
|
|
717
|
+
url: 'https://old.example.com/webhook',
|
|
718
|
+
events: ['user.created'],
|
|
719
|
+
secret: 'whsec_test_old',
|
|
720
|
+
active: true,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
await webhooksCollection.insert(webhook)
|
|
724
|
+
|
|
725
|
+
const updated = await webhooksCollection.update(
|
|
726
|
+
{ id: 'webhook-update-1' },
|
|
727
|
+
{
|
|
728
|
+
url: 'https://new.example.com/webhook',
|
|
729
|
+
events: ['user.created', 'user.deleted'],
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
expect(updated).toHaveLength(1)
|
|
734
|
+
expect(updated[0].url).toBe('https://new.example.com/webhook')
|
|
735
|
+
expect(updated[0].events).toHaveLength(2)
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('disables a webhook', async () => {
|
|
739
|
+
const webhook: Webhook = {
|
|
740
|
+
id: 'webhook-disable-1',
|
|
741
|
+
url: 'https://example.com/webhook',
|
|
742
|
+
events: ['user.created'],
|
|
743
|
+
secret: 'whsec_test_abc',
|
|
744
|
+
active: true,
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
await webhooksCollection.insert(webhook)
|
|
748
|
+
|
|
749
|
+
const updated = await webhooksCollection.update(
|
|
750
|
+
{ id: 'webhook-disable-1' },
|
|
751
|
+
{ active: false }
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
expect(updated[0].active).toBe(false)
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('deletes a webhook', async () => {
|
|
758
|
+
const webhook: Webhook = {
|
|
759
|
+
id: 'webhook-delete-1',
|
|
760
|
+
url: 'https://example.com/webhook',
|
|
761
|
+
events: ['test'],
|
|
762
|
+
secret: 'whsec_test_delete',
|
|
763
|
+
active: true,
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
await webhooksCollection.insert(webhook)
|
|
767
|
+
await webhooksCollection.delete({ id: 'webhook-delete-1' })
|
|
768
|
+
|
|
769
|
+
const found = await webhooksCollection.findOne({ id: 'webhook-delete-1' })
|
|
770
|
+
expect(found).toBeNull()
|
|
771
|
+
})
|
|
772
|
+
})
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// Teams Collection Tests
|
|
777
|
+
// ============================================================================
|
|
778
|
+
|
|
779
|
+
describe('Teams Collection', () => {
|
|
780
|
+
let teamsCollection: ReturnType<typeof TeamsCollection>
|
|
781
|
+
|
|
782
|
+
beforeEach(() => {
|
|
783
|
+
teamsCollection = TeamsCollection()
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
describe('schema validation', () => {
|
|
787
|
+
it('validates a valid team object', () => {
|
|
788
|
+
const validTeam: Team = {
|
|
789
|
+
id: 'team-1',
|
|
790
|
+
name: 'Engineering',
|
|
791
|
+
members: ['user-1', 'user-2', 'user-3'],
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const result = TeamSchema.safeParse(validTeam)
|
|
795
|
+
expect(result.success).toBe(true)
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
it('requires id field', () => {
|
|
799
|
+
const invalidTeam = {
|
|
800
|
+
name: 'Engineering',
|
|
801
|
+
members: ['user-1'],
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const result = TeamSchema.safeParse(invalidTeam)
|
|
805
|
+
expect(result.success).toBe(false)
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
it('requires name field', () => {
|
|
809
|
+
const invalidTeam = {
|
|
810
|
+
id: 'team-1',
|
|
811
|
+
members: ['user-1'],
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const result = TeamSchema.safeParse(invalidTeam)
|
|
815
|
+
expect(result.success).toBe(false)
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('requires members as array of user IDs', () => {
|
|
819
|
+
const invalidTeam = {
|
|
820
|
+
id: 'team-1',
|
|
821
|
+
name: 'Engineering',
|
|
822
|
+
members: 'user-1',
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const result = TeamSchema.safeParse(invalidTeam)
|
|
826
|
+
expect(result.success).toBe(false)
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
it('allows empty members array', () => {
|
|
830
|
+
const team: Team = {
|
|
831
|
+
id: 'team-empty',
|
|
832
|
+
name: 'Future Team',
|
|
833
|
+
members: [],
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const result = TeamSchema.safeParse(team)
|
|
837
|
+
expect(result.success).toBe(true)
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
describe('CRUD operations', () => {
|
|
842
|
+
it('inserts a new team', async () => {
|
|
843
|
+
const newTeam: Team = {
|
|
844
|
+
id: 'team-insert-1',
|
|
845
|
+
name: 'Product',
|
|
846
|
+
members: ['user-1', 'user-2'],
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const inserted = await teamsCollection.insert(newTeam)
|
|
850
|
+
expect(inserted.id).toBe('team-insert-1')
|
|
851
|
+
expect(inserted.name).toBe('Product')
|
|
852
|
+
expect(inserted.members).toHaveLength(2)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('finds teams by id', async () => {
|
|
856
|
+
const team: Team = {
|
|
857
|
+
id: 'team-find-1',
|
|
858
|
+
name: 'Design',
|
|
859
|
+
members: ['user-designer-1'],
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
await teamsCollection.insert(team)
|
|
863
|
+
const found = await teamsCollection.findOne({ id: 'team-find-1' })
|
|
864
|
+
|
|
865
|
+
expect(found).not.toBeNull()
|
|
866
|
+
expect(found?.name).toBe('Design')
|
|
867
|
+
expect(found?.members).toContain('user-designer-1')
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it('finds all teams', async () => {
|
|
871
|
+
const teams: Team[] = [
|
|
872
|
+
{ id: 'team-1', name: 'Engineering', members: ['user-1'] },
|
|
873
|
+
{ id: 'team-2', name: 'Product', members: ['user-2'] },
|
|
874
|
+
{ id: 'team-3', name: 'Design', members: ['user-3'] },
|
|
875
|
+
]
|
|
876
|
+
|
|
877
|
+
for (const team of teams) {
|
|
878
|
+
await teamsCollection.insert(team)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const allTeams = await teamsCollection.findMany()
|
|
882
|
+
expect(allTeams).toHaveLength(3)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
it('updates team name', async () => {
|
|
886
|
+
const team: Team = {
|
|
887
|
+
id: 'team-update-1',
|
|
888
|
+
name: 'Old Name',
|
|
889
|
+
members: ['user-1'],
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
await teamsCollection.insert(team)
|
|
893
|
+
|
|
894
|
+
const updated = await teamsCollection.update(
|
|
895
|
+
{ id: 'team-update-1' },
|
|
896
|
+
{ name: 'New Name' }
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
expect(updated).toHaveLength(1)
|
|
900
|
+
expect(updated[0].name).toBe('New Name')
|
|
901
|
+
expect(updated[0].members).toEqual(['user-1'])
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
it('adds a member to team', async () => {
|
|
905
|
+
const team: Team = {
|
|
906
|
+
id: 'team-members-1',
|
|
907
|
+
name: 'Engineering',
|
|
908
|
+
members: ['user-1', 'user-2'],
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
await teamsCollection.insert(team)
|
|
912
|
+
|
|
913
|
+
const updated = await teamsCollection.update(
|
|
914
|
+
{ id: 'team-members-1' },
|
|
915
|
+
{ members: ['user-1', 'user-2', 'user-3'] }
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
expect(updated[0].members).toHaveLength(3)
|
|
919
|
+
expect(updated[0].members).toContain('user-3')
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
it('removes a member from team', async () => {
|
|
923
|
+
const team: Team = {
|
|
924
|
+
id: 'team-remove-1',
|
|
925
|
+
name: 'Engineering',
|
|
926
|
+
members: ['user-1', 'user-2', 'user-3'],
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
await teamsCollection.insert(team)
|
|
930
|
+
|
|
931
|
+
const updated = await teamsCollection.update(
|
|
932
|
+
{ id: 'team-remove-1' },
|
|
933
|
+
{ members: ['user-1', 'user-3'] }
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
expect(updated[0].members).toHaveLength(2)
|
|
937
|
+
expect(updated[0].members).not.toContain('user-2')
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
it('deletes a team', async () => {
|
|
941
|
+
const team: Team = {
|
|
942
|
+
id: 'team-delete-1',
|
|
943
|
+
name: 'To Delete',
|
|
944
|
+
members: ['user-1'],
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
await teamsCollection.insert(team)
|
|
948
|
+
await teamsCollection.delete({ id: 'team-delete-1' })
|
|
949
|
+
|
|
950
|
+
const found = await teamsCollection.findOne({ id: 'team-delete-1' })
|
|
951
|
+
expect(found).toBeNull()
|
|
952
|
+
})
|
|
953
|
+
})
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
// ============================================================================
|
|
957
|
+
// Usage Collection Tests
|
|
958
|
+
// ============================================================================
|
|
959
|
+
|
|
960
|
+
describe('Usage Collection', () => {
|
|
961
|
+
let usageCollection: ReturnType<typeof UsageCollection>
|
|
962
|
+
|
|
963
|
+
beforeEach(() => {
|
|
964
|
+
usageCollection = UsageCollection()
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
describe('schema validation', () => {
|
|
968
|
+
it('validates a valid usage object', () => {
|
|
969
|
+
const validUsage: Usage = {
|
|
970
|
+
id: 'usage-1',
|
|
971
|
+
metric: 'api_calls',
|
|
972
|
+
value: 1500,
|
|
973
|
+
timestamp: new Date('2024-01-15T10:30:00Z'),
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const result = UsageSchema.safeParse(validUsage)
|
|
977
|
+
expect(result.success).toBe(true)
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
it('requires id field', () => {
|
|
981
|
+
const invalidUsage = {
|
|
982
|
+
metric: 'api_calls',
|
|
983
|
+
value: 1500,
|
|
984
|
+
timestamp: new Date(),
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const result = UsageSchema.safeParse(invalidUsage)
|
|
988
|
+
expect(result.success).toBe(false)
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
it('requires metric field', () => {
|
|
992
|
+
const invalidUsage = {
|
|
993
|
+
id: 'usage-1',
|
|
994
|
+
value: 1500,
|
|
995
|
+
timestamp: new Date(),
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const result = UsageSchema.safeParse(invalidUsage)
|
|
999
|
+
expect(result.success).toBe(false)
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
it('requires value as number', () => {
|
|
1003
|
+
const invalidUsage = {
|
|
1004
|
+
id: 'usage-1',
|
|
1005
|
+
metric: 'api_calls',
|
|
1006
|
+
value: '1500',
|
|
1007
|
+
timestamp: new Date(),
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const result = UsageSchema.safeParse(invalidUsage)
|
|
1011
|
+
expect(result.success).toBe(false)
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
it('requires timestamp as Date', () => {
|
|
1015
|
+
const invalidUsage = {
|
|
1016
|
+
id: 'usage-1',
|
|
1017
|
+
metric: 'api_calls',
|
|
1018
|
+
value: 1500,
|
|
1019
|
+
timestamp: '2024-01-15T10:30:00Z',
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const result = UsageSchema.safeParse(invalidUsage)
|
|
1023
|
+
expect(result.success).toBe(false)
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
it('allows positive and zero values', () => {
|
|
1027
|
+
const validUsages: Usage[] = [
|
|
1028
|
+
{
|
|
1029
|
+
id: 'usage-positive',
|
|
1030
|
+
metric: 'api_calls',
|
|
1031
|
+
value: 1500,
|
|
1032
|
+
timestamp: new Date(),
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'usage-zero',
|
|
1036
|
+
metric: 'errors',
|
|
1037
|
+
value: 0,
|
|
1038
|
+
timestamp: new Date(),
|
|
1039
|
+
},
|
|
1040
|
+
]
|
|
1041
|
+
|
|
1042
|
+
validUsages.forEach((usage) => {
|
|
1043
|
+
const result = UsageSchema.safeParse(usage)
|
|
1044
|
+
expect(result.success).toBe(true)
|
|
1045
|
+
})
|
|
1046
|
+
})
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
describe('CRUD operations', () => {
|
|
1050
|
+
it('inserts a new usage record', async () => {
|
|
1051
|
+
const newUsage: Usage = {
|
|
1052
|
+
id: 'usage-insert-1',
|
|
1053
|
+
metric: 'api_calls',
|
|
1054
|
+
value: 2500,
|
|
1055
|
+
timestamp: new Date('2024-01-15T10:00:00Z'),
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const inserted = await usageCollection.insert(newUsage)
|
|
1059
|
+
expect(inserted.id).toBe('usage-insert-1')
|
|
1060
|
+
expect(inserted.metric).toBe('api_calls')
|
|
1061
|
+
expect(inserted.value).toBe(2500)
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
it('finds usage records by id', async () => {
|
|
1065
|
+
const usage: Usage = {
|
|
1066
|
+
id: 'usage-find-1',
|
|
1067
|
+
metric: 'bandwidth',
|
|
1068
|
+
value: 5000,
|
|
1069
|
+
timestamp: new Date(),
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
await usageCollection.insert(usage)
|
|
1073
|
+
const found = await usageCollection.findOne({ id: 'usage-find-1' })
|
|
1074
|
+
|
|
1075
|
+
expect(found).not.toBeNull()
|
|
1076
|
+
expect(found?.metric).toBe('bandwidth')
|
|
1077
|
+
expect(found?.value).toBe(5000)
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
it('finds usage records by metric', async () => {
|
|
1081
|
+
const usageRecords: Usage[] = [
|
|
1082
|
+
{
|
|
1083
|
+
id: 'usage-1',
|
|
1084
|
+
metric: 'api_calls',
|
|
1085
|
+
value: 1000,
|
|
1086
|
+
timestamp: new Date('2024-01-01'),
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
id: 'usage-2',
|
|
1090
|
+
metric: 'api_calls',
|
|
1091
|
+
value: 1500,
|
|
1092
|
+
timestamp: new Date('2024-01-02'),
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
id: 'usage-3',
|
|
1096
|
+
metric: 'bandwidth',
|
|
1097
|
+
value: 5000,
|
|
1098
|
+
timestamp: new Date('2024-01-01'),
|
|
1099
|
+
},
|
|
1100
|
+
]
|
|
1101
|
+
|
|
1102
|
+
for (const record of usageRecords) {
|
|
1103
|
+
await usageCollection.insert(record)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const apiCalls = await usageCollection.findMany({
|
|
1107
|
+
where: { metric: 'api_calls' },
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
expect(apiCalls).toHaveLength(2)
|
|
1111
|
+
apiCalls.forEach((record) => {
|
|
1112
|
+
expect(record.metric).toBe('api_calls')
|
|
1113
|
+
})
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
it('sorts usage by timestamp descending', async () => {
|
|
1117
|
+
const usageRecords: Usage[] = [
|
|
1118
|
+
{
|
|
1119
|
+
id: 'usage-old',
|
|
1120
|
+
metric: 'api_calls',
|
|
1121
|
+
value: 1000,
|
|
1122
|
+
timestamp: new Date('2024-01-01T10:00:00Z'),
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
id: 'usage-new',
|
|
1126
|
+
metric: 'api_calls',
|
|
1127
|
+
value: 2000,
|
|
1128
|
+
timestamp: new Date('2024-01-15T10:00:00Z'),
|
|
1129
|
+
},
|
|
1130
|
+
]
|
|
1131
|
+
|
|
1132
|
+
for (const record of usageRecords) {
|
|
1133
|
+
await usageCollection.insert(record)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const sorted = await usageCollection.findMany({
|
|
1137
|
+
orderBy: { timestamp: 'desc' },
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
expect(sorted[0].id).toBe('usage-new')
|
|
1141
|
+
expect(sorted[1].id).toBe('usage-old')
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
it('updates usage value', async () => {
|
|
1145
|
+
const usage: Usage = {
|
|
1146
|
+
id: 'usage-update-1',
|
|
1147
|
+
metric: 'api_calls',
|
|
1148
|
+
value: 1000,
|
|
1149
|
+
timestamp: new Date(),
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
await usageCollection.insert(usage)
|
|
1153
|
+
|
|
1154
|
+
const updated = await usageCollection.update(
|
|
1155
|
+
{ id: 'usage-update-1' },
|
|
1156
|
+
{ value: 1500 }
|
|
1157
|
+
)
|
|
1158
|
+
|
|
1159
|
+
expect(updated).toHaveLength(1)
|
|
1160
|
+
expect(updated[0].value).toBe(1500)
|
|
1161
|
+
expect(updated[0].metric).toBe('api_calls')
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
it('deletes a usage record', async () => {
|
|
1165
|
+
const usage: Usage = {
|
|
1166
|
+
id: 'usage-delete-1',
|
|
1167
|
+
metric: 'api_calls',
|
|
1168
|
+
value: 1000,
|
|
1169
|
+
timestamp: new Date(),
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
await usageCollection.insert(usage)
|
|
1173
|
+
await usageCollection.delete({ id: 'usage-delete-1' })
|
|
1174
|
+
|
|
1175
|
+
const found = await usageCollection.findOne({ id: 'usage-delete-1' })
|
|
1176
|
+
expect(found).toBeNull()
|
|
1177
|
+
})
|
|
1178
|
+
|
|
1179
|
+
it('filters by value range', async () => {
|
|
1180
|
+
const usageRecords: Usage[] = [
|
|
1181
|
+
{
|
|
1182
|
+
id: 'usage-small',
|
|
1183
|
+
metric: 'errors',
|
|
1184
|
+
value: 10,
|
|
1185
|
+
timestamp: new Date(),
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
id: 'usage-medium',
|
|
1189
|
+
metric: 'errors',
|
|
1190
|
+
value: 500,
|
|
1191
|
+
timestamp: new Date(),
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
id: 'usage-large',
|
|
1195
|
+
metric: 'errors',
|
|
1196
|
+
value: 2000,
|
|
1197
|
+
timestamp: new Date(),
|
|
1198
|
+
},
|
|
1199
|
+
]
|
|
1200
|
+
|
|
1201
|
+
for (const record of usageRecords) {
|
|
1202
|
+
await usageCollection.insert(record)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const highUsage = await usageCollection.findMany({
|
|
1206
|
+
where: { value: { $gt: 100 } },
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
expect(highUsage).toHaveLength(2)
|
|
1210
|
+
expect(highUsage.every((r) => r.value > 100)).toBe(true)
|
|
1211
|
+
})
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1214
|
+
describe('subscriptions', () => {
|
|
1215
|
+
it('subscribes to usage collection changes', async () => {
|
|
1216
|
+
let lastData: Usage[] | null = null
|
|
1217
|
+
|
|
1218
|
+
const unsubscribe = usageCollection.subscribe((data) => {
|
|
1219
|
+
lastData = data
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
const usage: Usage = {
|
|
1223
|
+
id: 'usage-sub-1',
|
|
1224
|
+
metric: 'api_calls',
|
|
1225
|
+
value: 1000,
|
|
1226
|
+
timestamp: new Date(),
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
await usageCollection.insert(usage)
|
|
1230
|
+
|
|
1231
|
+
expect(lastData).not.toBeNull()
|
|
1232
|
+
expect(lastData).toHaveLength(1)
|
|
1233
|
+
expect(lastData![0].id).toBe('usage-sub-1')
|
|
1234
|
+
|
|
1235
|
+
unsubscribe()
|
|
1236
|
+
})
|
|
1237
|
+
})
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
// Cross-Collection Integration Tests
|
|
1242
|
+
// ============================================================================
|
|
1243
|
+
|
|
1244
|
+
describe('SaaS Collections Integration', () => {
|
|
1245
|
+
describe('type safety', () => {
|
|
1246
|
+
it('enforces type consistency for User objects', () => {
|
|
1247
|
+
const user: User = {
|
|
1248
|
+
id: 'user-1',
|
|
1249
|
+
name: 'Alice',
|
|
1250
|
+
email: 'alice@example.com',
|
|
1251
|
+
role: 'admin',
|
|
1252
|
+
createdAt: new Date(),
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
expect(user.role).toMatch(/admin|user|viewer/)
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
it('enforces type consistency for APIKey objects', () => {
|
|
1259
|
+
const key: APIKey = {
|
|
1260
|
+
id: 'key-1',
|
|
1261
|
+
key: 'sk_test_abc',
|
|
1262
|
+
name: 'Test',
|
|
1263
|
+
permissions: ['read:api'],
|
|
1264
|
+
expiresAt: new Date(),
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
expect(Array.isArray(key.permissions)).toBe(true)
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
it('enforces type consistency for Webhook objects', () => {
|
|
1271
|
+
const webhook: Webhook = {
|
|
1272
|
+
id: 'webhook-1',
|
|
1273
|
+
url: 'https://example.com/webhook',
|
|
1274
|
+
events: ['user.created'],
|
|
1275
|
+
secret: 'whsec_test_abc',
|
|
1276
|
+
active: true,
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
expect(typeof webhook.active).toBe('boolean')
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
it('enforces type consistency for Team objects', () => {
|
|
1283
|
+
const team: Team = {
|
|
1284
|
+
id: 'team-1',
|
|
1285
|
+
name: 'Engineering',
|
|
1286
|
+
members: ['user-1', 'user-2'],
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
expect(Array.isArray(team.members)).toBe(true)
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
it('enforces type consistency for Usage objects', () => {
|
|
1293
|
+
const usage: Usage = {
|
|
1294
|
+
id: 'usage-1',
|
|
1295
|
+
metric: 'api_calls',
|
|
1296
|
+
value: 1500,
|
|
1297
|
+
timestamp: new Date(),
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
expect(typeof usage.value).toBe('number')
|
|
1301
|
+
expect(usage.timestamp instanceof Date).toBe(true)
|
|
1302
|
+
})
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
describe('error handling', () => {
|
|
1306
|
+
it('validates schema strictly with safeParse', () => {
|
|
1307
|
+
const invalidUser = {
|
|
1308
|
+
id: 'user-1',
|
|
1309
|
+
name: 'Alice',
|
|
1310
|
+
email: 'invalid-email',
|
|
1311
|
+
role: 'admin',
|
|
1312
|
+
createdAt: new Date(),
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
const result = UserSchema.safeParse(invalidUser)
|
|
1316
|
+
expect(result.success).toBe(false)
|
|
1317
|
+
if (!result.success) {
|
|
1318
|
+
expect(result.error.issues).toBeDefined()
|
|
1319
|
+
expect(result.error.issues.length).toBeGreaterThan(0)
|
|
1320
|
+
}
|
|
1321
|
+
})
|
|
1322
|
+
|
|
1323
|
+
it('provides detailed error information', () => {
|
|
1324
|
+
const invalidKey = {
|
|
1325
|
+
id: 'key-1',
|
|
1326
|
+
key: 'sk_test_abc',
|
|
1327
|
+
name: 'Test',
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const result = APIKeySchema.safeParse(invalidKey)
|
|
1331
|
+
expect(result.success).toBe(false)
|
|
1332
|
+
if (!result.success) {
|
|
1333
|
+
expect(result.error.issues.length).toBeGreaterThanOrEqual(2)
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
})
|
|
1337
|
+
})
|