forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import * as v from 'valibot'
3import {
4 PackageNameSchema,
5 NewPackageNameSchema,
6 UsernameSchema,
7 OrgNameSchema,
8 ScopeTeamSchema,
9 OrgRoleSchema,
10 PermissionSchema,
11 OperationTypeSchema,
12 OtpSchema,
13 HexTokenSchema,
14 OperationIdSchema,
15 ConnectBodySchema,
16 ExecuteBodySchema,
17 CreateOperationBodySchema,
18 BatchOperationsBodySchema,
19 OrgAddUserParamsSchema,
20 AccessGrantParamsSchema,
21 PackageInitParamsSchema,
22 safeParse,
23 validateOperationParams,
24} from '../../../cli/src/schemas.ts'
25
26describe('PackageNameSchema', () => {
27 it('accepts valid package names', () => {
28 expect(v.safeParse(PackageNameSchema, 'my-package').success).toBe(true)
29 expect(v.safeParse(PackageNameSchema, '@scope/package').success).toBe(true)
30 expect(v.safeParse(PackageNameSchema, 'package123').success).toBe(true)
31 expect(v.safeParse(PackageNameSchema, 'lodash').success).toBe(true)
32 })
33
34 it('accepts legacy package names (uppercase)', () => {
35 // Legacy packages with uppercase are valid for old packages
36 expect(v.safeParse(PackageNameSchema, 'UPPERCASE').success).toBe(true)
37 })
38
39 it('rejects invalid package names', () => {
40 expect(v.safeParse(PackageNameSchema, '').success).toBe(false)
41 expect(v.safeParse(PackageNameSchema, '.package').success).toBe(false)
42 expect(v.safeParse(PackageNameSchema, '_package').success).toBe(false)
43 expect(v.safeParse(PackageNameSchema, ' spaces ').success).toBe(false)
44 })
45})
46
47describe('NewPackageNameSchema', () => {
48 it('accepts valid new package names', () => {
49 expect(v.safeParse(NewPackageNameSchema, 'my-package').success).toBe(true)
50 expect(v.safeParse(NewPackageNameSchema, '@scope/package').success).toBe(true)
51 expect(v.safeParse(NewPackageNameSchema, 'package123').success).toBe(true)
52 expect(v.safeParse(NewPackageNameSchema, 'lodash').success).toBe(true)
53 })
54
55 it('rejects legacy package name formats', () => {
56 // New packages must be lowercase
57 expect(v.safeParse(NewPackageNameSchema, 'UPPERCASE').success).toBe(false)
58 expect(v.safeParse(NewPackageNameSchema, 'MixedCase').success).toBe(false)
59 })
60
61 it('rejects invalid package names', () => {
62 expect(v.safeParse(NewPackageNameSchema, '').success).toBe(false)
63 expect(v.safeParse(NewPackageNameSchema, '.package').success).toBe(false)
64 expect(v.safeParse(NewPackageNameSchema, '_package').success).toBe(false)
65 expect(v.safeParse(NewPackageNameSchema, ' spaces ').success).toBe(false)
66 })
67})
68
69describe('PackageInitParamsSchema', () => {
70 it('accepts valid new package names', () => {
71 expect(v.safeParse(PackageInitParamsSchema, { name: 'my-package' }).success).toBe(true)
72 expect(
73 v.safeParse(PackageInitParamsSchema, { name: '@scope/pkg', author: 'alice' }).success,
74 ).toBe(true)
75 })
76
77 it('rejects legacy package name formats for new packages', () => {
78 // Cannot create new packages with uppercase names
79 expect(v.safeParse(PackageInitParamsSchema, { name: 'UPPERCASE' }).success).toBe(false)
80 expect(v.safeParse(PackageInitParamsSchema, { name: 'MixedCase' }).success).toBe(false)
81 })
82})
83
84describe('UsernameSchema', () => {
85 it('accepts valid usernames', () => {
86 expect(v.safeParse(UsernameSchema, 'alice').success).toBe(true)
87 expect(v.safeParse(UsernameSchema, 'bob123').success).toBe(true)
88 expect(v.safeParse(UsernameSchema, 'my-user').success).toBe(true)
89 })
90
91 it('rejects invalid usernames', () => {
92 expect(v.safeParse(UsernameSchema, '').success).toBe(false)
93 expect(v.safeParse(UsernameSchema, 'a'.repeat(51)).success).toBe(false)
94 expect(v.safeParse(UsernameSchema, '-user').success).toBe(false)
95 expect(v.safeParse(UsernameSchema, 'user-').success).toBe(false)
96 expect(v.safeParse(UsernameSchema, 'user name').success).toBe(false)
97 expect(v.safeParse(UsernameSchema, 'user;rm').success).toBe(false)
98 })
99})
100
101describe('OrgNameSchema', () => {
102 it('accepts valid org names', () => {
103 expect(v.safeParse(OrgNameSchema, 'nuxt').success).toBe(true)
104 expect(v.safeParse(OrgNameSchema, 'my-org').success).toBe(true)
105 })
106
107 it('rejects invalid org names', () => {
108 expect(v.safeParse(OrgNameSchema, '').success).toBe(false)
109 expect(v.safeParse(OrgNameSchema, 'a'.repeat(51)).success).toBe(false)
110 })
111})
112
113describe('ScopeTeamSchema', () => {
114 it('accepts valid scope:team format', () => {
115 expect(v.safeParse(ScopeTeamSchema, '@nuxt:developers').success).toBe(true)
116 expect(v.safeParse(ScopeTeamSchema, '@my-org:my-team').success).toBe(true)
117 expect(v.safeParse(ScopeTeamSchema, '@a:b').success).toBe(true)
118 })
119
120 it('rejects invalid scope:team format', () => {
121 expect(v.safeParse(ScopeTeamSchema, '').success).toBe(false)
122 expect(v.safeParse(ScopeTeamSchema, 'nuxt:developers').success).toBe(false) // missing @
123 expect(v.safeParse(ScopeTeamSchema, '@nuxt').success).toBe(false) // missing :team
124 expect(v.safeParse(ScopeTeamSchema, '@:team').success).toBe(false) // empty scope
125 expect(v.safeParse(ScopeTeamSchema, '@org:').success).toBe(false) // empty team
126 expect(v.safeParse(ScopeTeamSchema, '@-org:team').success).toBe(false) // scope starts with hyphen
127 expect(v.safeParse(ScopeTeamSchema, '@org:-team').success).toBe(false) // team starts with hyphen
128 })
129})
130
131describe('OrgRoleSchema', () => {
132 it('accepts valid roles', () => {
133 expect(v.safeParse(OrgRoleSchema, 'developer').success).toBe(true)
134 expect(v.safeParse(OrgRoleSchema, 'admin').success).toBe(true)
135 expect(v.safeParse(OrgRoleSchema, 'owner').success).toBe(true)
136 })
137
138 it('rejects invalid roles', () => {
139 expect(v.safeParse(OrgRoleSchema, 'user').success).toBe(false)
140 expect(v.safeParse(OrgRoleSchema, '').success).toBe(false)
141 expect(v.safeParse(OrgRoleSchema, 'ADMIN').success).toBe(false)
142 })
143})
144
145describe('PermissionSchema', () => {
146 it('accepts valid permissions', () => {
147 expect(v.safeParse(PermissionSchema, 'read-only').success).toBe(true)
148 expect(v.safeParse(PermissionSchema, 'read-write').success).toBe(true)
149 })
150
151 it('rejects invalid permissions', () => {
152 expect(v.safeParse(PermissionSchema, 'write').success).toBe(false)
153 expect(v.safeParse(PermissionSchema, '').success).toBe(false)
154 })
155})
156
157describe('OperationTypeSchema', () => {
158 it('accepts valid operation types', () => {
159 expect(v.safeParse(OperationTypeSchema, 'org:add-user').success).toBe(true)
160 expect(v.safeParse(OperationTypeSchema, 'team:create').success).toBe(true)
161 expect(v.safeParse(OperationTypeSchema, 'access:grant').success).toBe(true)
162 expect(v.safeParse(OperationTypeSchema, 'owner:add').success).toBe(true)
163 expect(v.safeParse(OperationTypeSchema, 'package:init').success).toBe(true)
164 })
165
166 it('rejects invalid operation types', () => {
167 expect(v.safeParse(OperationTypeSchema, 'invalid').success).toBe(false)
168 expect(v.safeParse(OperationTypeSchema, '').success).toBe(false)
169 })
170})
171
172describe('OtpSchema', () => {
173 it('accepts valid OTP codes', () => {
174 const result = v.safeParse(OtpSchema, '123456')
175 expect(result.success).toBe(true)
176 expect(result.output).toBe('123456')
177 })
178
179 it('accepts undefined (optional)', () => {
180 const result = v.safeParse(OtpSchema, undefined)
181 expect(result.success).toBe(true)
182 expect(result.output).toBeUndefined()
183 })
184
185 it('rejects invalid OTP codes', () => {
186 expect(v.safeParse(OtpSchema, '12345').success).toBe(false) // too short
187 expect(v.safeParse(OtpSchema, '1234567').success).toBe(false) // too long
188 expect(v.safeParse(OtpSchema, 'abcdef').success).toBe(false) // not digits
189 expect(v.safeParse(OtpSchema, '').success).toBe(false) // empty
190 })
191})
192
193describe('HexTokenSchema', () => {
194 it('accepts valid hex tokens', () => {
195 expect(v.safeParse(HexTokenSchema, 'abcd1234').success).toBe(true)
196 expect(v.safeParse(HexTokenSchema, 'ABCD1234').success).toBe(true)
197 expect(v.safeParse(HexTokenSchema, 'a1b2c3d4e5f6').success).toBe(true)
198 })
199
200 it('rejects invalid hex tokens', () => {
201 expect(v.safeParse(HexTokenSchema, '').success).toBe(false)
202 expect(v.safeParse(HexTokenSchema, 'ghij').success).toBe(false) // invalid hex chars
203 expect(v.safeParse(HexTokenSchema, 'abc-123').success).toBe(false) // contains hyphen
204 })
205})
206
207describe('OperationIdSchema', () => {
208 it('accepts valid 16-char hex operation IDs', () => {
209 expect(v.safeParse(OperationIdSchema, 'abcd1234abcd1234').success).toBe(true)
210 expect(v.safeParse(OperationIdSchema, '0123456789abcdef').success).toBe(true)
211 })
212
213 it('rejects invalid operation IDs', () => {
214 expect(v.safeParse(OperationIdSchema, '').success).toBe(false)
215 expect(v.safeParse(OperationIdSchema, 'abcd1234').success).toBe(false) // too short
216 expect(v.safeParse(OperationIdSchema, 'abcd1234abcd1234abcd').success).toBe(false) // too long
217 expect(v.safeParse(OperationIdSchema, 'ghij1234abcd1234').success).toBe(false) // invalid hex
218 })
219})
220
221describe('ConnectBodySchema', () => {
222 it('accepts valid connect body', () => {
223 const result = v.safeParse(ConnectBodySchema, { token: 'abcd1234' })
224 expect(result.success).toBe(true)
225 })
226
227 it('rejects invalid connect body', () => {
228 expect(v.safeParse(ConnectBodySchema, {}).success).toBe(false)
229 expect(v.safeParse(ConnectBodySchema, { token: '' }).success).toBe(false)
230 expect(v.safeParse(ConnectBodySchema, { token: 'invalid!' }).success).toBe(false)
231 })
232})
233
234describe('ExecuteBodySchema', () => {
235 it('accepts valid execute body with OTP', () => {
236 const result = v.safeParse(ExecuteBodySchema, { otp: '123456' })
237 expect(result.success).toBe(true)
238 })
239
240 it('accepts execute body without OTP', () => {
241 const result = v.safeParse(ExecuteBodySchema, {})
242 expect(result.success).toBe(true)
243 })
244
245 it('rejects invalid OTP', () => {
246 expect(v.safeParse(ExecuteBodySchema, { otp: '12345' }).success).toBe(false)
247 })
248
249 it('accepts interactive flag', () => {
250 const result = v.safeParse(ExecuteBodySchema, { interactive: true })
251 expect(result.success).toBe(true)
252 expect((result as { output: { interactive: boolean } }).output.interactive).toBe(true)
253 })
254
255 it('accepts openUrls flag', () => {
256 const result = v.safeParse(ExecuteBodySchema, { openUrls: true })
257 expect(result.success).toBe(true)
258 expect((result as { output: { openUrls: boolean } }).output.openUrls).toBe(true)
259 })
260
261 it('accepts all fields together', () => {
262 const result = v.safeParse(ExecuteBodySchema, {
263 otp: '123456',
264 interactive: true,
265 openUrls: false,
266 })
267 expect(result.success).toBe(true)
268 const output = (result as { output: { otp: string; interactive: boolean; openUrls: boolean } })
269 .output
270 expect(output.otp).toBe('123456')
271 expect(output.interactive).toBe(true)
272 expect(output.openUrls).toBe(false)
273 })
274
275 it('interactive and openUrls are optional (undefined when omitted)', () => {
276 const result = v.safeParse(ExecuteBodySchema, { otp: '123456' })
277 expect(result.success).toBe(true)
278 const output = (result as { output: Record<string, unknown> }).output
279 expect(output.interactive).toBeUndefined()
280 expect(output.openUrls).toBeUndefined()
281 })
282
283 it('rejects non-boolean values for interactive', () => {
284 expect(v.safeParse(ExecuteBodySchema, { interactive: 'true' }).success).toBe(false)
285 expect(v.safeParse(ExecuteBodySchema, { interactive: 1 }).success).toBe(false)
286 })
287
288 it('rejects non-boolean values for openUrls', () => {
289 expect(v.safeParse(ExecuteBodySchema, { openUrls: 'true' }).success).toBe(false)
290 expect(v.safeParse(ExecuteBodySchema, { openUrls: 1 }).success).toBe(false)
291 })
292})
293
294describe('CreateOperationBodySchema', () => {
295 it('accepts valid operation body', () => {
296 const result = v.safeParse(CreateOperationBodySchema, {
297 type: 'org:add-user',
298 params: { org: 'nuxt', user: 'alice', role: 'developer' },
299 description: 'Add alice to nuxt org',
300 command: 'npm org set nuxt alice developer',
301 })
302 expect(result.success).toBe(true)
303 })
304
305 it('rejects missing required fields', () => {
306 expect(v.safeParse(CreateOperationBodySchema, {}).success).toBe(false)
307 expect(v.safeParse(CreateOperationBodySchema, { type: 'org:add-user' }).success).toBe(false)
308 })
309
310 it('rejects invalid type', () => {
311 expect(
312 v.safeParse(CreateOperationBodySchema, {
313 type: 'invalid',
314 params: {},
315 description: 'test',
316 command: 'test',
317 }).success,
318 ).toBe(false)
319 })
320})
321
322describe('BatchOperationsBodySchema', () => {
323 it('accepts array of valid operations', () => {
324 const result = v.safeParse(BatchOperationsBodySchema, [
325 {
326 type: 'org:add-user',
327 params: { org: 'nuxt', user: 'alice', role: 'developer' },
328 description: 'Add alice',
329 command: 'npm org set nuxt alice developer',
330 },
331 {
332 type: 'org:add-user',
333 params: { org: 'nuxt', user: 'bob', role: 'admin' },
334 description: 'Add bob',
335 command: 'npm org set nuxt bob admin',
336 },
337 ])
338 expect(result.success).toBe(true)
339 })
340
341 it('accepts empty array', () => {
342 expect(v.safeParse(BatchOperationsBodySchema, []).success).toBe(true)
343 })
344
345 it('rejects array with invalid operation', () => {
346 expect(
347 v.safeParse(BatchOperationsBodySchema, [
348 { type: 'invalid', params: {}, description: 'test', command: 'test' },
349 ]).success,
350 ).toBe(false)
351 })
352})
353
354describe('OrgAddUserParamsSchema', () => {
355 it('accepts valid org:add-user params', () => {
356 const result = v.safeParse(OrgAddUserParamsSchema, {
357 org: 'nuxt',
358 user: 'alice',
359 role: 'developer',
360 })
361 expect(result.success).toBe(true)
362 })
363
364 it('rejects invalid params', () => {
365 expect(v.safeParse(OrgAddUserParamsSchema, {}).success).toBe(false)
366 expect(v.safeParse(OrgAddUserParamsSchema, { org: 'nuxt' }).success).toBe(false)
367 expect(
368 v.safeParse(OrgAddUserParamsSchema, {
369 org: 'nuxt',
370 user: 'alice',
371 role: 'invalid',
372 }).success,
373 ).toBe(false)
374 })
375})
376
377describe('AccessGrantParamsSchema', () => {
378 it('accepts valid access:grant params', () => {
379 const result = v.safeParse(AccessGrantParamsSchema, {
380 permission: 'read-write',
381 scopeTeam: '@nuxt:developers',
382 pkg: '@nuxt/kit',
383 })
384 expect(result.success).toBe(true)
385 })
386
387 it('rejects invalid params', () => {
388 expect(
389 v.safeParse(AccessGrantParamsSchema, {
390 permission: 'write', // invalid permission
391 scopeTeam: '@nuxt:developers',
392 pkg: '@nuxt/kit',
393 }).success,
394 ).toBe(false)
395 })
396})
397
398describe('safeParse helper', () => {
399 it('returns success with data for valid input', () => {
400 const result = safeParse(UsernameSchema, 'alice')
401 expect(result.success).toBe(true)
402 expect(result).toHaveProperty('data', 'alice')
403 })
404
405 it('returns error message for invalid input', () => {
406 const result = safeParse(UsernameSchema, '')
407 expect(result.success).toBe(false)
408 expect(result).toHaveProperty('error', 'Username is required')
409 })
410
411 it('includes path in error message for nested objects', () => {
412 const result = safeParse(OrgAddUserParamsSchema, { org: '', user: 'alice', role: 'developer' })
413 expect(result.success).toBe(false)
414 expect((result as { error: string }).error).toContain('org')
415 })
416})
417
418describe('validateOperationParams', () => {
419 it('validates org:add-user params', () => {
420 expect(() =>
421 validateOperationParams('org:add-user', {
422 org: 'nuxt',
423 user: 'alice',
424 role: 'developer',
425 }),
426 ).not.toThrow()
427 })
428
429 it('throws for invalid org:add-user params', () => {
430 expect(() => validateOperationParams('org:add-user', { org: 'nuxt' })).toThrow('Invalid key')
431 })
432
433 it('validates team:create params', () => {
434 expect(() =>
435 validateOperationParams('team:create', {
436 scopeTeam: '@nuxt:developers',
437 }),
438 ).not.toThrow()
439 })
440
441 it('validates access:grant params', () => {
442 expect(() =>
443 validateOperationParams('access:grant', {
444 permission: 'read-write',
445 scopeTeam: '@nuxt:developers',
446 pkg: '@nuxt/kit',
447 }),
448 ).not.toThrow()
449 })
450
451 it('validates owner:add params', () => {
452 expect(() =>
453 validateOperationParams('owner:add', {
454 user: 'alice',
455 pkg: '@nuxt/kit',
456 }),
457 ).not.toThrow()
458 })
459
460 it('validates package:init params', () => {
461 expect(() =>
462 validateOperationParams('package:init', {
463 name: 'my-package',
464 }),
465 ).not.toThrow()
466
467 expect(() =>
468 validateOperationParams('package:init', {
469 name: 'my-package',
470 author: 'alice',
471 }),
472 ).not.toThrow()
473 })
474})