npm-registry.test.ts 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. // SDK 包与推送工具的 npm 仓库(/npm/):包信息、最新版本、包文件、作用域名称的两种写法、安全审计
  2. import { execFileSync } from 'node:child_process'
  3. import crypto from 'node:crypto'
  4. import fs from 'node:fs'
  5. import os from 'node:os'
  6. import path from 'node:path'
  7. import Fastify from 'fastify'
  8. import { afterAll, beforeAll, describe, expect, it } from 'vitest'
  9. import { NpmRegistry, registerNpmRegistry } from '../src/http/npm-registry'
  10. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-registry-'))
  11. const f = Fastify()
  12. function pack(manifest: Record<string, unknown>, file: string) {
  13. const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-registry-stage-'))
  14. fs.mkdirSync(path.join(stage, 'package'))
  15. fs.writeFileSync(path.join(stage, 'package', 'package.json'), JSON.stringify(manifest))
  16. fs.writeFileSync(path.join(stage, 'package', 'index.js'), 'export {}\n')
  17. execFileSync('tar', ['czf', path.join(dir, file), 'package'], { cwd: stage })
  18. }
  19. beforeAll(async () => {
  20. pack({ name: '@agent-server/client', version: '0.5.0' }, 'agent-server-client-0.5.0.tgz')
  21. pack({ name: '@agent-server/client', version: '0.10.0', peerDependencies: { '@agent-server/shared': '0.10.0' } }, 'agent-server-client-0.10.0.tgz')
  22. pack({ name: '@agent-server/skills-cli', version: '0.3.0', bin: { 'agent-skills': './agent-skills.mjs' } }, 'agent-server-skills-cli-0.3.0.tgz')
  23. fs.copyFileSync(path.join(dir, 'agent-server-skills-cli-0.3.0.tgz'), path.join(dir, 'agent-skills.tgz')) // 不带版本号的文件不算
  24. fs.writeFileSync(path.join(dir, 'broken-1.0.0.tgz'), 'not a tarball')
  25. registerNpmRegistry(f, new NpmRegistry(() => dir))
  26. await f.ready()
  27. })
  28. afterAll(() => f.close())
  29. describe('npm 仓库', () => {
  30. it('包信息:全部版本、最新版本按数字比较、对等依赖与 bin 原样保留、校验值与文件一致', async () => {
  31. for (const url of ['/npm/@agent-server%2fclient', '/npm/@agent-server%2Fclient', '/npm/@agent-server/client']) {
  32. const r = await f.inject({ url, headers: { host: 'agent.example.com' } })
  33. expect(r.statusCode).toBe(200)
  34. expect(r.headers['cache-control']).toBe('no-cache')
  35. const doc = r.json()
  36. expect(doc['dist-tags']).toEqual({ latest: '0.10.0' })
  37. expect(Object.keys(doc.versions).sort()).toEqual(['0.10.0', '0.5.0'])
  38. expect(doc.versions['0.10.0'].peerDependencies).toEqual({ '@agent-server/shared': '0.10.0' })
  39. expect(doc.versions['0.10.0'].dist.tarball).toBe('http://agent.example.com/npm/-/agent-server-client-0.10.0.tgz')
  40. }
  41. const cli = (await f.inject({ url: '/npm/@agent-server%2fskills-cli' })).json()
  42. expect(Object.keys(cli.versions)).toEqual(['0.3.0'])
  43. expect(cli.versions['0.3.0'].bin).toEqual({ 'agent-skills': './agent-skills.mjs' })
  44. const tgz = await f.inject({ url: '/npm/-/agent-server-skills-cli-0.3.0.tgz' })
  45. expect(tgz.statusCode).toBe(200)
  46. expect(tgz.headers['cache-control']).toContain('immutable')
  47. expect(cli.versions['0.3.0'].dist.integrity).toBe(`sha512-${crypto.createHash('sha512').update(tgz.rawPayload).digest('base64')}`)
  48. })
  49. it('不存在的包与文件为 404;旧的固定文件名不提供;安全审计返回空结果;最新版本列表', async () => {
  50. expect((await f.inject({ url: '/npm/@agent-server%2fnope' })).statusCode).toBe(404)
  51. expect((await f.inject({ url: '/npm/-/agent-skills.tgz' })).statusCode).toBe(404)
  52. expect((await f.inject({ url: '/npm/-/..%2f..%2fetc%2fpasswd' })).statusCode).toBe(404)
  53. expect((await f.inject({ method: 'POST', url: '/npm/-/npm/v1/security/advisories/bulk', payload: {} })).json()).toEqual({})
  54. expect((await f.inject({ url: '/npm/-/latest' })).json()).toEqual(expect.arrayContaining([
  55. { name: '@agent-server/client', version: '0.10.0', bin: false },
  56. { name: '@agent-server/skills-cli', version: '0.3.0', bin: true },
  57. ]))
  58. })
  59. })