mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 10:11:42 -04:00
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2
- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
`it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
instead of relying on `@types/jest` ambient declarations. Under the
new tsgo build, `import { jest } from '@jest/globals'` shadows the
ambient `jest` namespace, breaking `@types/jest`'s `declare var
describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
now import from it, and add `@types/node` to packages that need it
but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
no longer globally available.
* chore: fix remaining tsgo type-strictness errors
- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
toStrictEqual / toEqual; @jest/globals rejects the typed objects
(which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.
* chore: address Copilot review on #11332
- Move misplaced `@jest/globals` imports to the top import block in
checkEngine, run.ts, and workspace/root-finder tests where the
script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
in bins/linker, lockfile/fs, and resolving/local-resolver tests with
`await expect(x()).rejects.toMatchObject({...})`. The old pattern
swallowed an unrelated `throw` if the under-test call silently
succeeded, which would fail on the catch-block assertion with a
misleading message.
502 lines
16 KiB
TypeScript
502 lines
16 KiB
TypeScript
import { describe, expect, it } from '@jest/globals'
|
|
import {
|
|
pollForWebAuthToken,
|
|
type WebAuthContext,
|
|
type WebAuthFetchOptions,
|
|
type WebAuthFetchResponse,
|
|
WebAuthTimeoutError,
|
|
} from '@pnpm/network.web-auth'
|
|
|
|
function createMockResponse (init: {
|
|
ok: boolean
|
|
status: number
|
|
json?: unknown
|
|
headers?: WebAuthFetchResponse['headers']
|
|
}): WebAuthFetchResponse {
|
|
let bodyConsumed = false
|
|
return {
|
|
ok: init.ok,
|
|
status: init.status,
|
|
json: async () => {
|
|
if (bodyConsumed) throw new Error('Unexpected double consumption of response body')
|
|
bodyConsumed = true
|
|
return init.json ?? {}
|
|
},
|
|
headers: init.headers ?? {
|
|
get: name => {
|
|
throw new Error(`Unexpected call to headers.get: ${name}`)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
const createMockContext = (overrides?: Partial<WebAuthContext>): WebAuthContext => ({
|
|
Date: { now: () => 0 },
|
|
setTimeout: (cb: () => void) => cb(),
|
|
fetch: async () => createMockResponse({
|
|
ok: false,
|
|
status: 404,
|
|
}),
|
|
...overrides,
|
|
})
|
|
|
|
const fetchOptions: WebAuthFetchOptions = { method: 'GET' }
|
|
|
|
describe('pollForWebAuthToken', () => {
|
|
it('returns token when doneUrl responds with 200 and token', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount < 3) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => '1' },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'web-token-123' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('web-token-123')
|
|
expect(fetchCallCount).toBe(3)
|
|
})
|
|
|
|
it('passes doneUrl and fetchOptions to fetch', async () => {
|
|
const capturedArgs: Array<{ url: string, options: WebAuthFetchOptions }> = []
|
|
const opts: WebAuthFetchOptions = {
|
|
method: 'GET',
|
|
timeout: 5000,
|
|
retry: { retries: 3 },
|
|
}
|
|
const context = createMockContext({
|
|
fetch: async (url: string, options: WebAuthFetchOptions): Promise<WebAuthFetchResponse> => {
|
|
capturedArgs.push({ url, options })
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.example.com/done', fetchOptions: opts })
|
|
expect(capturedArgs).toEqual([{ url: 'https://registry.example.com/done', options: opts }])
|
|
})
|
|
|
|
it('respects Retry-After header when polling', async () => {
|
|
const setTimeoutDelays: number[] = []
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: (name: string) => name === 'retry-after' ? '5' : null },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
// First setTimeout is the default 1s poll interval,
|
|
// second is the additional delay (5s Retry-After minus the 1s already waited),
|
|
// third is the default 1s poll interval for the next iteration.
|
|
expect(setTimeoutDelays).toStrictEqual([1000, 4000, 1000])
|
|
})
|
|
|
|
it('ignores Retry-After when value is not a finite number', async () => {
|
|
const setTimeoutDelays: number[] = []
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => 'not-a-number' },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
// Only the default 1s poll intervals, no additional Retry-After delay.
|
|
expect(setTimeoutDelays).toStrictEqual([1000, 1000])
|
|
})
|
|
|
|
it('ignores Retry-After when value is null', async () => {
|
|
const setTimeoutDelays: number[] = []
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => null },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(setTimeoutDelays).toStrictEqual([1000, 1000])
|
|
})
|
|
|
|
it('skips additional delay when Retry-After is less than poll interval', async () => {
|
|
const setTimeoutDelays: number[] = []
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: (name: string) => name === 'retry-after' ? '0.5' : null },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
// Retry-After of 0.5s (500ms) is less than the 1s poll interval already waited,
|
|
// so no additional delay is added.
|
|
expect(setTimeoutDelays).toStrictEqual([1000, 1000])
|
|
})
|
|
|
|
it('caps Retry-After additional delay to remaining timeout', async () => {
|
|
let time = 0
|
|
const setTimeoutDelays: number[] = []
|
|
const context = createMockContext({
|
|
Date: { now: () => time },
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
time += ms
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
json: { token: 'tok' },
|
|
headers: { get: (name: string) => name === 'retry-after' ? '60' : null },
|
|
}),
|
|
})
|
|
// Use a 10s timeout so the 60s Retry-After gets capped.
|
|
await expect(pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions, timeoutMs: 10_000 }))
|
|
.rejects.toBeInstanceOf(WebAuthTimeoutError)
|
|
// The first delay is the 1s poll interval. The additional delay from
|
|
// Retry-After (59s) should be capped to the remaining timeout (~9s).
|
|
expect(setTimeoutDelays[0]).toBe(1000)
|
|
expect(setTimeoutDelays[1]).toBeLessThanOrEqual(9000)
|
|
})
|
|
|
|
it('throws WebAuthTimeoutError when timeout expires during Retry-After wait', async () => {
|
|
let time = 0
|
|
const timeoutMs = 5000
|
|
const context = createMockContext({
|
|
Date: {
|
|
now: () => time,
|
|
},
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
time += ms
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
// After the 1s poll interval, time is 1000.
|
|
// Remaining is 4000. Retry-After is 100s, so additional is 99000,
|
|
// capped to 4000. After that wait, time = 5000, which equals timeout.
|
|
// Next iteration: now - startTime > timeoutMs → throw.
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: (name: string) => name === 'retry-after' ? '100' : null },
|
|
})
|
|
},
|
|
})
|
|
await expect(pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions, timeoutMs }))
|
|
.rejects.toMatchObject({ timeout: timeoutMs })
|
|
})
|
|
|
|
it('continues polling when fetch throws', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
throw new Error('network failure')
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('tok')
|
|
expect(fetchCallCount).toBe(2)
|
|
})
|
|
|
|
it('continues polling when response is not ok', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: false,
|
|
status: 404,
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('tok')
|
|
expect(fetchCallCount).toBe(2)
|
|
})
|
|
|
|
it('continues polling when response.json() throws', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return {
|
|
headers: { get: () => null },
|
|
json: async () => {
|
|
throw new Error('invalid json')
|
|
},
|
|
ok: true,
|
|
status: 200,
|
|
}
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('tok')
|
|
expect(fetchCallCount).toBe(2)
|
|
})
|
|
|
|
it('continues polling when response body has no token', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { something: 'else' },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('tok')
|
|
expect(fetchCallCount).toBe(2)
|
|
})
|
|
|
|
it('continues polling when token is empty string', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: '' },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'real-tok' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('real-tok')
|
|
expect(fetchCallCount).toBe(2)
|
|
})
|
|
|
|
it('throws WebAuthTimeoutError after timeout', async () => {
|
|
let time = 0
|
|
const context = createMockContext({
|
|
Date: { now: () => time },
|
|
setTimeout: (cb: () => void) => {
|
|
time += 6 * 60 * 1000 // Jump past timeout
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => null },
|
|
}),
|
|
})
|
|
await expect(pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions }))
|
|
.rejects.toBeInstanceOf(WebAuthTimeoutError)
|
|
})
|
|
|
|
it('uses custom timeout value', async () => {
|
|
let time = 0
|
|
const customTimeoutMs = 3000
|
|
const context = createMockContext({
|
|
Date: { now: () => time },
|
|
setTimeout: (cb: () => void) => {
|
|
time += 2000
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => null },
|
|
}),
|
|
})
|
|
await expect(pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions, timeoutMs: customTimeoutMs }))
|
|
.rejects.toMatchObject({ timeout: customTimeoutMs })
|
|
})
|
|
|
|
it('recovers after multiple consecutive fetch errors', async () => {
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount <= 5) {
|
|
throw new Error(`failure #${fetchCallCount}`)
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'recovered' },
|
|
})
|
|
},
|
|
})
|
|
const token = await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
expect(token).toBe('recovered')
|
|
expect(fetchCallCount).toBe(6)
|
|
})
|
|
|
|
it('waits pollIntervalMs before each fetch call', async () => {
|
|
const setTimeoutDelays: number[] = []
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
setTimeoutDelays.push(ms)
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount < 4) {
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => null },
|
|
})
|
|
}
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 200,
|
|
json: { token: 'tok' },
|
|
})
|
|
},
|
|
})
|
|
await pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions })
|
|
// Each iteration waits 1000ms before fetching.
|
|
expect(setTimeoutDelays).toStrictEqual([1000, 1000, 1000, 1000])
|
|
})
|
|
|
|
it('throws WebAuthTimeoutError immediately when remaining time is zero during Retry-After', async () => {
|
|
let time = 0
|
|
const timeoutMs = 2000
|
|
let fetchCallCount = 0
|
|
const context = createMockContext({
|
|
Date: { now: () => time },
|
|
setTimeout: (cb: () => void, ms: number) => {
|
|
time += ms
|
|
cb()
|
|
},
|
|
fetch: async (): Promise<WebAuthFetchResponse> => {
|
|
fetchCallCount++
|
|
if (fetchCallCount === 1) {
|
|
// After poll interval (1s), time = 1000, remaining = 1000.
|
|
// Retry-After = 10s → additional = 9000 > remaining.
|
|
// Capped to remaining (1000). After that wait, time = 2000.
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: (name: string) => name === 'retry-after' ? '10' : null },
|
|
})
|
|
}
|
|
// This second fetch still returns 202, but the next timeout check
|
|
// should trigger the error since time (2000) - start (0) = 2000 > 2000? No, it's equal.
|
|
// Actually the condition is `>` so 2000 > 2000 is false. So it waits another 1s, then 3000 > 2000 is true.
|
|
return createMockResponse({
|
|
ok: true,
|
|
status: 202,
|
|
headers: { get: () => null },
|
|
})
|
|
},
|
|
})
|
|
await expect(pollForWebAuthToken({ context, doneUrl: 'https://registry.npmjs.org/auth/done', fetchOptions, timeoutMs }))
|
|
.rejects.toMatchObject({ timeout: timeoutMs })
|
|
})
|
|
})
|