[READ-ONLY] a fast, modern browser for the npm registry
at main 474 lines 17 kB view raw
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})