[READ-ONLY] Mirror of https://github.com/danielroe/provenance-action. Fail CI when dependencies in your lockfile lose npm provenance or trusted publisher status
github-actions provenance security trusted-publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

provenance-action / test / lockfile.test.ts
23 kB 673 lines
1import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' 2import { tmpdir } from 'node:os' 3import { join } from 'node:path' 4import { describe, expect, it } from 'vitest' 5import { 6 detectLockfile, 7 diffDependencySets, 8 findLockfileLine, 9 matchesTrustPolicyExclude, 10 parseBunLock, 11 parseLockfile, 12 parseNpmLock, 13 parsePnpmLock, 14 parsePnpmWorkspaceConfig, 15 parseTrustPolicyExclude, 16 parseYarnBerryLock, 17 parseYarnV1Lock, 18 readTextFile, 19 yarnBerrySpecifierToName, 20 yarnV1SpecifierToName, 21} from '../lib/lockfile.ts' 22 23describe('lockfile utils', () => { 24 it('detectLockfile finds supported lockfiles', () => { 25 const dir = mkdtempSync(join(tmpdir(), 'oidc-action-')) 26 try { 27 // No file 28 expect(detectLockfile(dir)).toBe(undefined) 29 // Create pnpm first 30 writeFileSync(join(dir, 'pnpm-lock.yaml'), '') 31 expect(detectLockfile(dir)).toBe('pnpm-lock.yaml') 32 // If pnpm exists, it should still prefer that even if others are present 33 writeFileSync(join(dir, 'package-lock.json'), '{}') 34 writeFileSync(join(dir, 'yarn.lock'), '') 35 expect(detectLockfile(dir)).toBe('pnpm-lock.yaml') 36 } 37 finally { 38 rmSync(dir, { recursive: true, force: true }) 39 } 40 }) 41 42 it('readTextFile reads utf8 content', () => { 43 const dir = mkdtempSync(join(tmpdir(), 'oidc-action-')) 44 try { 45 const p = join(dir, 'file.txt') 46 writeFileSync(p, 'hello', 'utf8') 47 expect(readTextFile(p)).toBe('hello') 48 } 49 finally { 50 rmSync(dir, { recursive: true, force: true }) 51 } 52 }) 53 54 // Test parseLockfile with unsupported file types 55 it('parseLockfile returns empty map for unsupported lockfile types', () => { 56 const result = parseLockfile('unsupported.lock', 'content') 57 expect(result).toBeInstanceOf(Map) 58 expect(result.size).toBe(0) 59 }) 60 61 // Test yarnV1SpecifierToName edge cases 62 it('yarnV1SpecifierToName handles specs without @', () => { 63 expect(yarnV1SpecifierToName('invalid-spec')).toBe(undefined) 64 }) 65 66 it('yarnV1SpecifierToName handles specs with @ at position 0', () => { 67 expect(yarnV1SpecifierToName('@invalid')).toBe(undefined) 68 }) 69 70 // Test yarnBerrySpecifierToName edge cases 71 it('yarnBerrySpecifierToName handles scoped packages without version', () => { 72 expect(yarnBerrySpecifierToName('@scope/name')).toBe(undefined) 73 }) 74 75 it('yarnBerrySpecifierToName handles regular packages without version', () => { 76 expect(yarnBerrySpecifierToName('name')).toBe(undefined) 77 }) 78 79 it('yarnBerrySpecifierToName handles quoted specs', () => { 80 expect(yarnBerrySpecifierToName('"@scope/name@npm:1.0.0"')).toBe('@scope/name') 81 expect(yarnBerrySpecifierToName('\'test@npm:1.0.0\'')).toBe('test') 82 }) 83 84 // Test findLockfileLine edge cases 85 it('findLockfileLine returns undefined for unsupported lockfile types', () => { 86 const result = findLockfileLine('unsupported.lock', 'content', 'test', '1.0.0') 87 expect(result).toBe(undefined) 88 }) 89 90 it('findLockfileLine handles npm lockfile without matches', () => { 91 const content = '{"packages": {}}' 92 const result = findLockfileLine('package-lock.json', content, 'nonexistent', '1.0.0') 93 expect(result).toBe(undefined) 94 }) 95 96 it('findLockfileLine handles pnpm lockfile without matches', () => { 97 const content = 'packages:\n\n /other@1.0.0:\n resolution: {}' 98 const result = findLockfileLine('pnpm-lock.yaml', content, 'test', '1.0.0') 99 expect(result).toBe(undefined) 100 }) 101 102 it('findLockfileLine handles yarn v1 lockfile without matches', () => { 103 const content = '# yarn lockfile v1\n\nother@^1.0.0:\n version "1.0.0"' 104 const result = findLockfileLine('yarn.lock', content, 'test', '1.0.0') 105 expect(result).toBe(undefined) 106 }) 107 108 it('findLockfileLine handles yarn berry lockfile without matches', () => { 109 const content = '"other@npm:^1.0.0":\n version: "1.0.0"' 110 const result = findLockfileLine('yarn.lock', content, 'test', '1.0.0') 111 expect(result).toBe(undefined) 112 }) 113 114 it('findLockfileLine handles bun lockfile without matches', () => { 115 const content = '{"packages": {"different@2.0.0": {"version": "2.0.0"}}}' 116 const result = findLockfileLine('bun.lock', content, 'test', '1.0.0') 117 expect(result).toBe(undefined) 118 }) 119 120 // Test diffDependencySets to cover setsEqual edge cases 121 it('diffDependencySets detects different versions in sets', () => { 122 const prev = new Map([['lodash', new Set(['4.17.21'])]]) 123 const curr = new Map([['lodash', new Set(['4.17.22'])]]) 124 const diff = diffDependencySets(prev, curr) 125 expect(diff.length).toBe(1) 126 expect(diff[0].name).toBe('lodash') 127 }) 128 129 it('diffDependencySets detects removed packages', () => { 130 const prev = new Map([['lodash', new Set(['4.17.21'])], ['removed', new Set(['1.0.0'])]]) 131 const curr = new Map([['lodash', new Set(['4.17.21'])]]) 132 const diff = diffDependencySets(prev, curr) 133 expect(diff.length).toBe(1) 134 expect(diff[0].name).toBe('removed') 135 expect(diff[0].previous.has('1.0.0')).toBe(true) 136 expect(diff[0].current.size).toBe(0) 137 }) 138}) 139 140describe('parseNpmLock', () => { 141// Test parseNpmLock error cases 142 it('parseNpmLock handles malformed JSON', () => { 143 const result = parseNpmLock('invalid json {') 144 expect(result).toBeInstanceOf(Map) 145 expect(result.size).toBe(0) 146 }) 147 148 it('parseNpmLock handles missing packages property', () => { 149 const result = parseNpmLock('{}') 150 expect(result).toBeInstanceOf(Map) 151 expect(result.size).toBe(0) 152 }) 153 154 it('parseNpmLock handles entries without version', () => { 155 const content = '{"packages":{"node_modules/test":{"name":"test"}}}' 156 const result = parseNpmLock(content) 157 expect(result.size).toBe(0) 158 }) 159 160 it('parseNpmLock handles root package entry', () => { 161 const content = '{"packages":{"":{"name":"root","version":"1.0.0"}}}' 162 const result = parseNpmLock(content) 163 expect(result.size).toBe(0) // Root package should be skipped 164 }) 165 166 it('parseNpmLock handles entry without name', () => { 167 const content = '{"packages":{"node_modules/test":{"version":"1.0.0"}}}' 168 const result = parseNpmLock(content) 169 expect(result.get('test')).toContain('1.0.0') 170 }) 171 172 it('parseNpmLock handles entry with explicit name', () => { 173 const content = '{"packages":{"node_modules/test":{"name":"different","version":"1.0.0"}}}' 174 const result = parseNpmLock(content) 175 expect(result.get('different')).toContain('1.0.0') 176 }) 177}) 178 179describe('parsePnpmLock', () => { 180// Test parsePnpmLock edge cases 181 it('parsePnpmLock handles content without packages section', () => { 182 const content = 'settings:\n autoInstallPeers: true' 183 const result = parsePnpmLock(content) 184 expect(result.size).toBe(0) 185 }) 186 187 it('parsePnpmLock handles packages section without entries', () => { 188 const content = 'packages:\n\nsettings:\n autoInstallPeers: true' 189 const result = parsePnpmLock(content) 190 expect(result.size).toBe(0) 191 }) 192 193 it('parsePnpmLock handles malformed package entries', () => { 194 const content = `packages: 195 196 /test@1.0.0: 197 resolution: {integrity: sha512-test} 198 199 invalid-entry-without-colon 200 201 /valid@2.0.0: 202 resolution: {integrity: sha512-test}` 203 const result = parsePnpmLock(content) 204 // Both test and valid should be parsed because both have valid format 205 expect(result.get('test')).toContain('1.0.0') 206 expect(result.get('valid')).toContain('2.0.0') 207 expect(result.size).toBe(2) 208 }) 209 210 it('parsePnpmLock handles entries without @ symbol', () => { 211 const content = `packages: 212 213 /no-at-symbol: 214 resolution: {integrity: sha512-test}` 215 const result = parsePnpmLock(content) 216 expect(result.size).toBe(0) 217 }) 218 219 it('parsePnpmLock handles entries with @ at position 0', () => { 220 const content = `packages: 221 222 /@scoped: 223 resolution: {integrity: sha512-test}` 224 const result = parsePnpmLock(content) 225 expect(result.size).toBe(0) 226 }) 227 228 it('parsePnpmLock handles entries without version', () => { 229 const content = `packages: 230 231 /test@: 232 resolution: {integrity: sha512-test}` 233 const result = parsePnpmLock(content) 234 expect(result.size).toBe(0) 235 }) 236 237 it('parsePnpmLock handles quoted package names', () => { 238 const content = `packages: 239 240 "/test@1.0.0": 241 resolution: {integrity: sha512-test} 242 243 '/another@2.0.0': 244 resolution: {integrity: sha512-test}` 245 const result = parsePnpmLock(content) 246 expect(result.size).toBe(2) 247 // The parser doesn't strip leading slash for quoted names 248 expect(result.get('/test')).toContain('1.0.0') 249 expect(result.get('/another')).toContain('2.0.0') 250 }) 251 252 it('parsePnpmLock handles packages with peer dependencies suffix', () => { 253 const content = `packages: 254 255 /test@1.0.0(peer@2.0.0): 256 resolution: {integrity: sha512-test}` 257 const result = parsePnpmLock(content) 258 expect(result.get('test')).toContain('1.0.0') 259 }) 260}) 261 262describe('parseYarnV1Lock', () => { 263// Test parseYarnV1Lock edge cases 264 it('parseYarnV1Lock handles entries without version', () => { 265 const content = `# yarn lockfile v1 266 267test@^1.0.0: 268 resolved "https://registry.yarnpkg.com/test" 269 # no version field` 270 const result = parseYarnV1Lock(content) 271 expect(result.size).toBe(0) 272 }) 273 274 it('parseYarnV1Lock handles multi-line headers', () => { 275 const content = `# yarn lockfile v1 276 277"test@^1.0.0", 278"test@^1.1.0": 279 version "1.2.0" 280 resolved "https://registry.yarnpkg.com/test"` 281 const result = parseYarnV1Lock(content) 282 expect(result.get('test')).toContain('1.2.0') 283 }) 284 285 it('parseYarnV1Lock skips invalid specifiers', () => { 286 const content = `# yarn lockfile v1 287 288invalid-spec: 289 version "1.0.0" 290 291test@^1.0.0: 292 version "2.0.0"` 293 const result = parseYarnV1Lock(content) 294 expect(result.get('test')).toContain('2.0.0') 295 expect(result.size).toBe(1) 296 }) 297}) 298 299describe('parseYarnBerryLock', () => { 300// Test parseYarnBerryLock edge cases 301 it('parseYarnBerryLock handles entries without version', () => { 302 const content = `"test@npm:^1.0.0": 303 resolution: "test@npm:1.2.0" 304 # no version field` 305 const result = parseYarnBerryLock(content) 306 expect(result.size).toBe(0) 307 }) 308 309 it('parseYarnBerryLock handles invalid specifiers', () => { 310 const content = `"invalid-spec": 311 version: "1.0.0" 312 313"test@npm:^1.0.0": 314 version: "2.0.0"` 315 const result = parseYarnBerryLock(content) 316 expect(result.get('test')).toContain('2.0.0') 317 expect(result.size).toBe(1) 318 }) 319 320 it('parseYarnBerryLock skips comment lines', () => { 321 const content = `# This is a comment 322"test@npm:^1.0.0": 323 version: "1.0.0" 324 325# Another comment 326"other@npm:^2.0.0": 327 version: "2.0.0"` 328 const result = parseYarnBerryLock(content) 329 expect(result.get('test')).toContain('1.0.0') 330 expect(result.get('other')).toContain('2.0.0') 331 }) 332 333 it('parseYarnBerryLock handles single quotes in specifiers', () => { 334 const content = `'test@npm:^1.0.0': 335 version: '1.0.0'` 336 const result = parseYarnBerryLock(content) 337 expect(result.get('test')).toContain('1.0.0') 338 }) 339 340 it('parseYarnBerryLock handles lines without colon', () => { 341 const content = `"test@npm:^1.0.0" 342 version: "1.0.0" 343 344"valid@npm:^2.0.0": 345 version: "2.0.0"` 346 const result = parseYarnBerryLock(content) 347 // Only valid should be parsed since test line doesn't end with ':' 348 expect(result.get('valid')).toContain('2.0.0') 349 expect(result.has('test')).toBe(false) 350 expect(result.size).toBe(1) 351 }) 352}) 353 354describe('parseBunLock', () => { 355// Test parseBunLock edge cases 356 it('parseBunLock handles malformed JSON', () => { 357 const result = parseBunLock('invalid json {') 358 expect(result).toBeInstanceOf(Map) 359 expect(result.size).toBe(0) 360 }) 361 362 it('parseBunLock handles JSONC with comments', () => { 363 const content = `{ 364 // This is a comment 365 "packages": [ 366 { 367 "name": "test", 368 "version": "1.0.0" // Another comment 369 } 370 ] 371}` 372 const result = parseBunLock(content) 373 expect(result.get('test')).toContain('1.0.0') 374 }) 375 376 it('parseBunLock handles malformed JSONC', () => { 377 const content = `{ 378 // This is a comment 379 "packages": [ 380 invalid json 381 ] 382}` 383 const result = parseBunLock(content) 384 expect(result.size).toBe(0) 385 }) 386 387 it('parseBunLock handles missing packages property', () => { 388 const result = parseBunLock('{}') 389 expect(result.size).toBe(0) 390 }) 391 392 it('parseBunLock handles null packages', () => { 393 const result = parseBunLock('{"packages": null}') 394 expect(result.size).toBe(0) 395 }) 396 397 it('parseBunLock handles array format with null entries', () => { 398 const content = '{"packages": [null, {"name": "test", "version": "1.0.0"}]}' 399 const result = parseBunLock(content) 400 expect(result.get('test')).toContain('1.0.0') 401 expect(result.size).toBe(1) 402 }) 403 404 it('parseBunLock handles array format without name or version', () => { 405 const content = '{"packages": [{"description": "test"}]}' 406 const result = parseBunLock(content) 407 expect(result.size).toBe(0) 408 }) 409 410 it('parseBunLock handles object format without version', () => { 411 const content = '{"packages": {"test@1.0.0": {"name": "test"}}}' 412 const result = parseBunLock(content) 413 expect(result.size).toBe(0) 414 }) 415 416 it('parseBunLock handles object format without name but with key pattern', () => { 417 const content = '{"packages": {"test@1.0.0": {"version": "1.0.0"}}}' 418 const result = parseBunLock(content) 419 expect(result.get('test')).toContain('1.0.0') 420 }) 421 422 it('parseBunLock handles scoped packages in key pattern', () => { 423 const content = '{"packages": {"@scope/test@1.0.0": {"version": "1.0.0"}}}' 424 const result = parseBunLock(content) 425 expect(result.get('@scope/test')).toContain('1.0.0') 426 }) 427 428 it('parseBunLock handles invalid key patterns', () => { 429 const content = '{"packages": {"invalid-key": {"version": "1.0.0"}}}' 430 const result = parseBunLock(content) 431 expect(result.size).toBe(0) 432 }) 433 434 it('parseBunLock handles non-array non-object packages', () => { 435 const content = '{"packages": "invalid"}' 436 const result = parseBunLock(content) 437 expect(result.size).toBe(0) 438 }) 439}) 440 441describe('findLockfileLine', () => { 442 it('findLockfileLine finds npm lockfile by node_modules key', () => { 443 const content = '{"packages": {"node_modules/test": {"version": "1.0.0"}}}' 444 const line = findLockfileLine('package-lock.json', content, 'test', '1.0.0') 445 expect(typeof line).toBe('number') 446 }) 447 448 it('findLockfileLine finds npm lockfile by name field', () => { 449 const content = '{"packages": {"": {}, "x": {"name": "test", "version": "1.0.0"}}}' 450 const line = findLockfileLine('package-lock.json', content, 'test', '1.0.0') 451 expect(typeof line).toBe('number') 452 }) 453 454 it('findLockfileLine finds pnpm lockfile primary pattern', () => { 455 const content = 'packages:\n\n /test@1.0.0:\n resolution: {integrity: sha512-test}' 456 const line = findLockfileLine('pnpm-lock.yaml', content, 'test', '1.0.0') 457 expect(line).toBe(3) 458 }) 459 460 it('findLockfileLine finds pnpm lockfile secondary pattern (trimStart startsWith)', () => { 461 const content = 'packages:\n\n /test@1.0.0(peer@2.0.0):\n resolution: {integrity: sha512-test}' 462 const line = findLockfileLine('pnpm-lock.yaml', content, 'test', '1.0.0') 463 expect(typeof line).toBe('number') 464 }) 465 466 it('findLockfileLine finds yarn v1 lockfile matching version', () => { 467 const content = '# yarn lockfile v1\n\n"test@^1.0.0":\n version "1.0.0"\n\nother@^1.0.0:\n version "1.0.0"' 468 const line = findLockfileLine('yarn.lock', content, 'test', '1.0.0') 469 expect(typeof line).toBe('number') 470 }) 471 472 it('findLockfileLine in yarn v1 breaks when next header encountered', () => { 473 const content = '# yarn lockfile v1\n\n"test@^1.0.0":\n resolved "x"\nother@^1.0.0:\n version "1.0.0"' 474 const line = findLockfileLine('yarn.lock', content, 'test', '2.0.0') 475 expect(line).toBe(undefined) 476 }) 477 478 it('findLockfileLine finds yarn berry lockfile matching version', () => { 479 const content = '"test@npm:^1.0.0":\n version: "1.0.0"\n"other@npm:^1.0.0":\n version: "1.0.0"' 480 const line = findLockfileLine('yarn.lock', content, 'test', '1.0.0') 481 expect(typeof line).toBe('number') 482 }) 483 484 it('findLockfileLine finds bun lockfile by name then version and breaks at closing brace when not found', () => { 485 const content = '{\n "packages": [\n {\n "name": "test",\n "desc": "noop"\n }\n ]\n}' 486 const line = findLockfileLine('bun.lock', content, 'test', '9.9.9') 487 expect(line).toBeUndefined() 488 }) 489 490 it('findLockfileLine finds bun lockfile by array name and version', () => { 491 const content = '{\n "packages": [\n {\n "name": "test",\n "version": "1.0.0"\n }\n ]\n}' 492 const line = findLockfileLine('bun.lock', content, 'test', '1.0.0') 493 expect(typeof line).toBe('number') 494 }) 495 496 it('findLockfileLine finds bun lockfile by key pattern', () => { 497 const content = '{"packages": {"test@1.0.0": {"something": 1}}}' 498 const line = findLockfileLine('bun.lock', content, 'test', '1.0.0') 499 expect(typeof line).toBe('number') 500 }) 501 502 it('findLockfileLine finds bun lockfile by version regex fallback', () => { 503 const content = '{"x": 1}\n{"y": 2}\n "version": "1.0.0"\n{"z": 3}' 504 const line = findLockfileLine('bun.lock', content, 'noname', '1.0.0') 505 expect(typeof line).toBe('number') 506 }) 507}) 508 509describe('parsePnpmWorkspaceConfig', () => { 510 it('returns empty config when file does not exist', () => { 511 const dir = mkdtempSync(join(tmpdir(), 'provenance-action-')) 512 try { 513 const result = parsePnpmWorkspaceConfig(dir) 514 expect(result).toEqual({}) 515 } 516 finally { 517 rmSync(dir, { recursive: true, force: true }) 518 } 519 }) 520 521 it('parses trustPolicyExclude from pnpm-workspace.yaml', () => { 522 const dir = mkdtempSync(join(tmpdir(), 'provenance-action-')) 523 try { 524 const content = `trustPolicyExclude: 525 - 'chokidar@4.0.3' 526 - 'webpack@4.47.0 || 5.102.1' 527 - '@babel/core@7.28.5' 528` 529 writeFileSync(join(dir, 'pnpm-workspace.yaml'), content, 'utf8') 530 const result = parsePnpmWorkspaceConfig(dir) 531 expect(result.trustPolicyExclude).toEqual([ 532 'chokidar@4.0.3', 533 'webpack@4.47.0 || 5.102.1', 534 '@babel/core@7.28.5', 535 ]) 536 } 537 finally { 538 rmSync(dir, { recursive: true, force: true }) 539 } 540 }) 541}) 542 543describe('parseTrustPolicyExclude', () => { 544 it('parses list format with single quotes', () => { 545 const content = `trustPolicyExclude: 546 - 'chokidar@4.0.3' 547 - 'webpack' 548` 549 const result = parseTrustPolicyExclude(content) 550 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3', 'webpack']) 551 }) 552 553 it('parses list format with double quotes', () => { 554 const content = `trustPolicyExclude: 555 - "chokidar@4.0.3" 556 - "webpack@5.0.0" 557` 558 const result = parseTrustPolicyExclude(content) 559 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3', 'webpack@5.0.0']) 560 }) 561 562 it('parses list format without quotes', () => { 563 const content = `trustPolicyExclude: 564 - chokidar@4.0.3 565 - webpack 566` 567 const result = parseTrustPolicyExclude(content) 568 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3', 'webpack']) 569 }) 570 571 it('parses inline array format', () => { 572 const content = `trustPolicyExclude: ['chokidar@4.0.3', 'webpack']` 573 const result = parseTrustPolicyExclude(content) 574 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3', 'webpack']) 575 }) 576 577 it('parses inline array format with double quotes', () => { 578 const content = `trustPolicyExclude: ["chokidar@4.0.3", "webpack"]` 579 const result = parseTrustPolicyExclude(content) 580 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3', 'webpack']) 581 }) 582 583 it('handles empty content', () => { 584 const result = parseTrustPolicyExclude('') 585 expect(result).toEqual({}) 586 }) 587 588 it('handles content without trustPolicyExclude', () => { 589 const content = `packages: 590 - 'packages/*' 591` 592 const result = parseTrustPolicyExclude(content) 593 expect(result).toEqual({}) 594 }) 595 596 it('handles comments', () => { 597 const content = `# This is a comment 598trustPolicyExclude: 599 # Another comment 600 - chokidar@4.0.3 601` 602 const result = parseTrustPolicyExclude(content) 603 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3']) 604 }) 605 606 it('stops parsing at next top-level key', () => { 607 const content = `trustPolicyExclude: 608 - chokidar@4.0.3 609packages: 610 - 'packages/*' 611` 612 const result = parseTrustPolicyExclude(content) 613 expect(result.trustPolicyExclude).toEqual(['chokidar@4.0.3']) 614 }) 615 616 it('handles disjunction patterns', () => { 617 const content = `trustPolicyExclude: 618 - 'webpack@4.47.0 || 5.102.1' 619` 620 const result = parseTrustPolicyExclude(content) 621 expect(result.trustPolicyExclude).toEqual(['webpack@4.47.0 || 5.102.1']) 622 }) 623}) 624 625describe('matchesTrustPolicyExclude', () => { 626 it('matches package without version (any version)', () => { 627 const patterns = ['chokidar'] 628 expect(matchesTrustPolicyExclude('chokidar', '4.0.3', patterns)).toBe(true) 629 expect(matchesTrustPolicyExclude('chokidar', '3.0.0', patterns)).toBe(true) 630 expect(matchesTrustPolicyExclude('other', '1.0.0', patterns)).toBe(false) 631 }) 632 633 it('matches package with specific version', () => { 634 const patterns = ['chokidar@4.0.3'] 635 expect(matchesTrustPolicyExclude('chokidar', '4.0.3', patterns)).toBe(true) 636 expect(matchesTrustPolicyExclude('chokidar', '3.0.0', patterns)).toBe(false) 637 expect(matchesTrustPolicyExclude('other', '4.0.3', patterns)).toBe(false) 638 }) 639 640 it('matches scoped packages', () => { 641 const patterns = ['@babel/core@7.28.5'] 642 expect(matchesTrustPolicyExclude('@babel/core', '7.28.5', patterns)).toBe(true) 643 expect(matchesTrustPolicyExclude('@babel/core', '7.0.0', patterns)).toBe(false) 644 expect(matchesTrustPolicyExclude('@babel/preset-env', '7.28.5', patterns)).toBe(false) 645 }) 646 647 it('matches scoped packages without version', () => { 648 const patterns = ['@babel/core'] 649 expect(matchesTrustPolicyExclude('@babel/core', '7.28.5', patterns)).toBe(true) 650 expect(matchesTrustPolicyExclude('@babel/core', '7.0.0', patterns)).toBe(true) 651 expect(matchesTrustPolicyExclude('@babel/preset-env', '7.28.5', patterns)).toBe(false) 652 }) 653 654 it('matches disjunction patterns', () => { 655 const patterns = ['webpack@4.47.0 || 5.102.1'] 656 expect(matchesTrustPolicyExclude('webpack', '4.47.0', patterns)).toBe(true) 657 expect(matchesTrustPolicyExclude('webpack', '5.102.1', patterns)).toBe(true) 658 expect(matchesTrustPolicyExclude('webpack', '5.0.0', patterns)).toBe(false) 659 expect(matchesTrustPolicyExclude('other', '4.47.0', patterns)).toBe(false) 660 }) 661 662 it('matches multiple patterns', () => { 663 const patterns = ['chokidar@4.0.3', 'webpack', '@babel/core@7.28.5'] 664 expect(matchesTrustPolicyExclude('chokidar', '4.0.3', patterns)).toBe(true) 665 expect(matchesTrustPolicyExclude('webpack', '5.0.0', patterns)).toBe(true) 666 expect(matchesTrustPolicyExclude('@babel/core', '7.28.5', patterns)).toBe(true) 667 expect(matchesTrustPolicyExclude('lodash', '4.0.0', patterns)).toBe(false) 668 }) 669 670 it('returns false for empty patterns', () => { 671 expect(matchesTrustPolicyExclude('chokidar', '4.0.3', [])).toBe(false) 672 }) 673})