verify-no-bare-dispatcher.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { describe, expect, it } from 'vitest'
  2. import { findDispatcherViolations, scanRepository, DISPATCHER_OWNER } from './verify-no-bare-dispatcher.ts'
  3. const FILE = 'packages/web/web-fetch-http/src/network.ts'
  4. function reasons(source: string, file = FILE): string[] {
  5. return findDispatcherViolations(file, source).map(violation => violation.what)
  6. }
  7. describe('bare dispatcher check', () => {
  8. it('rejects the shape that silently bypassed the proxy before this rule existed', () => {
  9. expect(reasons(`
  10. import { Agent } from 'undici'
  11. const dispatcher = new Agent({ connect: { lookup } })
  12. const response = await fetch(url, { dispatcher })
  13. `)).toEqual(['constructs an undici agent', 'passes an explicit \`dispatcher\`'])
  14. })
  15. it('rejects an explicit dispatcher option however the agent was obtained', () => {
  16. expect(reasons(" const response = await fetch(url, { method: 'GET', dispatcher: pooled })"))
  17. .toEqual(['passes an explicit \`dispatcher\`'])
  18. })
  19. it('rejects a namespaced construction', () => {
  20. expect(reasons(`
  21. import * as undici from 'undici'
  22. const agent = new undici.ProxyAgent(uri)
  23. `)).toEqual(['constructs an undici agent'])
  24. })
  25. it('reports the offending line number and text', () => {
  26. const source = "import { Agent } from 'undici'\nconst a = 1\nconst b = new Agent({})"
  27. expect(findDispatcherViolations(FILE, source)).toEqual([
  28. { file: FILE, line: 3, what: 'constructs an undici agent', text: 'const b = new Agent({})' },
  29. ])
  30. })
  31. it('accepts the sanctioned factory', () => {
  32. expect(reasons(' const route = proxyRouteFor(url)')).toEqual([])
  33. })
  34. it('rejects the shorthand form a line-wise regex misses', () => {
  35. expect(reasons(`
  36. import { Agent } from 'undici'
  37. const dispatcher = pool
  38. const response = await fetch(url, { method: 'GET', dispatcher })
  39. `)).toEqual(['passes an explicit \`dispatcher\`'])
  40. })
  41. it('rejects a quoted dispatcher key', () => {
  42. expect(reasons(" await fetch(url, { 'dispatcher': pooled })"))
  43. .toEqual(['passes an explicit \`dispatcher\`'])
  44. })
  45. it('rejects construction through an import alias', () => {
  46. expect(reasons(`
  47. import { Agent as CustomAgent } from 'undici'
  48. const agent = new CustomAgent({})
  49. `)).toEqual(['constructs an undici agent'])
  50. })
  51. it('rejects a destructured dynamic import, the form this repository loads undici with', () => {
  52. expect(reasons(`
  53. const { Agent } = await import('undici')
  54. const agent = new Agent({})
  55. `)).toEqual(['constructs an undici agent'])
  56. })
  57. it('rejects a renamed binding from a dynamic import', () => {
  58. expect(reasons(`
  59. const { ProxyAgent: Tunnel } = await import('undici')
  60. const agent = new Tunnel({ uri })
  61. `)).toEqual(['constructs an undici agent'])
  62. })
  63. it('rejects a namespace bound by a dynamic import', () => {
  64. expect(reasons(`
  65. const undici = await import('undici')
  66. const agent = new undici.Agent({})
  67. `)).toEqual(['constructs an undici agent'])
  68. })
  69. it('finds a dynamic import nested inside a function, not only at the top level', () => {
  70. expect(reasons(`
  71. export async function requestWith(url) {
  72. const { Agent } = await import('undici')
  73. return new Agent({})
  74. }
  75. `)).toEqual(['constructs an undici agent'])
  76. })
  77. it('accepts a dynamic import of an unrelated module that exports Agent', () => {
  78. expect(reasons(`
  79. const { Agent } = await import('./our-own-agent.ts')
  80. const agent = new Agent({})
  81. `)).toEqual([])
  82. })
  83. it('accepts an unrelated class that happens to be named Agent', () => {
  84. expect(reasons(`
  85. import { Agent } from './our-own-agent.ts'
  86. const agent = new Agent({})
  87. `)).toEqual([])
  88. })
  89. it('accepts an exemption annotated on the line above, where a long line puts it', () => {
  90. expect(reasons(`
  91. import { Agent } from 'undici'
  92. // proxy-exempt: the dispatcher already applied the active policy.
  93. const response = await fetch(url, { method: 'GET', headers, dispatcher })
  94. `)).toEqual([])
  95. })
  96. it('accepts an annotated exemption', () => {
  97. expect(reasons(`
  98. import { Agent } from 'undici'
  99. const agent = new Agent({}) // proxy-exempt: loopback transport for the local test server
  100. `)).toEqual([])
  101. })
  102. it('exempts the package that owns dispatcher construction', () => {
  103. expect(reasons("import { EnvHttpProxyAgent } from 'undici'\nconst agent = new EnvHttpProxyAgent()", `${DISPATCHER_OWNER}src/install.ts`)).toEqual([])
  104. })
  105. it('normalizes native separators before exempting the owning package', () => {
  106. expect(reasons("import { Agent } from 'undici'\nconst agent = new Agent({})", DISPATCHER_OWNER.replaceAll('/', '\\') + 'src\\install.ts')).toEqual([])
  107. })
  108. it('parses only a file naming undici or the dispatcher option', () => {
  109. // The pre-filter that keeps this gate from parsing 1576 of 1597 repository files excludes a
  110. // file mentioning neither word. Both violations require one of them in source, so nothing
  111. // detectable is excluded — the second case proves a violating shape survives the filter.
  112. expect(reasons(' const agent = new Agent({ keepAlive: true })')).toEqual([])
  113. expect(reasons(`
  114. import { Agent } from 'undici'
  115. const agent = new Agent({})
  116. `)).toEqual(['constructs an undici agent'])
  117. })
  118. it('passes on the current tree', () => {
  119. expect(scanRepository()).toEqual([])
  120. })
  121. })