test(tarball-fetcher): migrate to Jest

ref #2858
This commit is contained in:
Zoltan Kochan
2020-11-08 17:20:32 +02:00
parent 5ff6c28fab
commit 0c5f1bcc9e
10 changed files with 142 additions and 134 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/tarball-fetcher": patch
---
Throw a better error message when a local tarball integrity check fails.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/error": minor
---
Every error object has an optional "attempts" field.

View File

@@ -1,11 +1,20 @@
export default class PnpmError extends Error {
public readonly code: string
public readonly hint?: string
public attempts?: number
public pkgsStack?: Array<{ id: string, name: string, version: string }>
constructor (code: string, message: string, opts?: { hint?: string }) {
constructor (
code: string,
message: string,
opts?: {
attempts?: number
hint?: string
}
) {
super(message)
this.code = `ERR_PNPM_${code}`
this.hint = opts?.hint
this.attempts = opts?.attempts
}
}

View File

@@ -0,0 +1 @@
module.exports = require('../../jest.config')

View File

@@ -11,7 +11,7 @@
"scripts": {
"lint": "eslint -c ../../eslint.json src/**/*.ts test/**/*.ts",
"prepublishOnly": "pnpm run compile",
"_test": "cd ../.. && c8 --reporter lcov --reports-dir packages/tarball-fetcher/coverage ts-node packages/tarball-fetcher/test --type-check",
"_test": "jest",
"test": "pnpm run compile && pnpm run _test",
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build"
},

View File

@@ -14,7 +14,7 @@ import ssri = require('ssri')
const BIG_TARBALL_SIZE = 1024 * 1024 * 5 // 5 MB
class TarballIntegrityError extends PnpmError {
export class TarballIntegrityError extends PnpmError {
public readonly found: string
public readonly expected: string
public readonly algorithm: string
@@ -22,13 +22,17 @@ class TarballIntegrityError extends PnpmError {
public readonly url: string
constructor (opts: {
attempts?: number
found: string
expected: string
algorithm: string
sri: string
url: string
}) {
super('TARBALL_INTEGRITY', `Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`)
super('TARBALL_INTEGRITY',
`Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`,
{ attempts: opts.attempts }
)
this.found = opts.found
this.expected = opts.expected
this.algorithm = opts.algorithm

View File

@@ -5,13 +5,16 @@ export default class BadTarballError extends PnpmError {
public receivedSize: number
constructor (
opts: {
attempts?: number
expectedSize: number
receivedSize: number
tarballUrl: string
}
) {
const message = `Actual size (${opts.receivedSize}) of tarball (${opts.tarballUrl}) did not match the one specified in 'Content-Length' header (${opts.expectedSize})`
super('BAD_TARBALL_SIZE', message)
super('BAD_TARBALL_SIZE', message, {
attempts: opts?.attempts,
})
this.expectedSize = opts.expectedSize
this.receivedSize = opts.receivedSize
}

View File

@@ -11,11 +11,18 @@ import {
GetCredentials,
RetryTimeoutOptions,
} from '@pnpm/fetching-types'
import createDownloader, { DownloadFunction } from './createDownloader'
import createDownloader, {
DownloadFunction,
TarballIntegrityError,
} from './createDownloader'
import path = require('path')
import fs = require('mz/fs')
import ssri = require('ssri')
export { BadTarballError } from './errorTypes'
export { TarballIntegrityError }
export default function (
fetchFromRegistry: FetchFromRegistry,
getCredentials: GetCredentials,
@@ -36,7 +43,7 @@ export default function (
}
}
function fetchFromTarball (
async function fetchFromTarball (
ctx: {
download: DownloadFunction
getCredentialsByURI: (registry: string) => {
@@ -101,8 +108,15 @@ async function fetchFromLocalTarball (
)
return { filesIndex: fetchResult }
} catch (err) {
err.attempts = 1
err.resource = tarball
throw err
const error = new TarballIntegrityError({
attempts: 1,
algorithm: err['algorithm'],
expected: err['expected'],
found: err['found'],
sri: err['sri'],
url: tarball,
})
error['resource'] = tarball
throw error
}
}

View File

@@ -1,17 +1,19 @@
/// <reference path="../../../typings/index.d.ts" />
import createCafs from '@pnpm/cafs'
import PnpmError, { FetchError } from '@pnpm/error'
import { createFetchFromRegistry } from '@pnpm/fetch'
import createFetcher from '@pnpm/tarball-fetcher'
import createFetcher, {
BadTarballError,
TarballIntegrityError,
} from '@pnpm/tarball-fetcher'
import path = require('path')
import cpFile = require('cp-file')
import fs = require('mz/fs')
import nock = require('nock')
import ssri = require('ssri')
import test = require('tape')
import tempy = require('tempy')
const cafsDir = tempy.directory()
console.log(cafsDir)
const cafs = createCafs(cafsDir)
const tarballPath = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-6.24.1.tgz')
@@ -28,7 +30,7 @@ const fetch = createFetcher(fetchFromRegistry, getCredentials, {
},
})
test('fail when tarball size does not match content-length', async t => {
test('fail when tarball size does not match content-length', async () => {
const scope = nock(registry)
.get('/foo.tgz')
.times(2)
@@ -37,7 +39,6 @@ test('fail when tarball size does not match content-length', async t => {
})
process.chdir(tempy.directory())
t.comment(`temp dir ${process.cwd()}`)
const resolution = {
// Even though the integrity of the downloaded tarball
@@ -48,24 +49,21 @@ test('fail when tarball size does not match content-length', async t => {
tarball: `${registry}foo.tgz`,
}
try {
await fetch.tarball(cafs, resolution, {
await expect(
fetch.tarball(cafs, resolution, {
lockfileDir: process.cwd(),
})
t.fail('should have failed')
} catch (err) {
t.equal(err.message, 'Actual size (1279) of tarball (http://example.com/foo.tgz) did not match the one specified in \'Content-Length\' header (1048576)')
t.equal(err['code'], 'ERR_PNPM_BAD_TARBALL_SIZE')
t.equal(err['expectedSize'], 1048576)
t.equal(err['receivedSize'], tarballSize)
t.equal(err['attempts'], 2)
t.ok(scope.isDone())
t.end()
}
).rejects.toThrow(
new BadTarballError({
expectedSize: 1048576,
receivedSize: tarballSize,
tarballUrl: resolution.tarball,
})
)
expect(scope.isDone()).toBeTruthy()
})
test('retry when tarball size does not match content-length', async t => {
test('retry when tarball size does not match content-length', async () => {
nock(registry)
.get('/foo.tgz')
.replyWithFile(200, tarballPath, {
@@ -79,7 +77,6 @@ test('retry when tarball size does not match content-length', async t => {
})
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const resolution = { tarball: 'http://example.com/foo.tgz' }
@@ -87,12 +84,11 @@ test('retry when tarball size does not match content-length', async t => {
lockfileDir: process.cwd(),
})
t.ok(result.filesIndex)
t.ok(nock.isDone())
t.end()
expect(result.filesIndex).toBeTruthy()
expect(nock.isDone()).toBeTruthy()
})
test('fail when integrity check fails two times in a row', async t => {
test('fail when integrity check fails two times in a row', async () => {
const scope = nock(registry)
.get('/foo.tgz')
.times(2)
@@ -101,31 +97,29 @@ test('fail when integrity check fails two times in a row', async t => {
})
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const resolution = {
integrity: tarballIntegrity,
tarball: 'http://example.com/foo.tgz',
}
try {
await fetch.tarball(cafs, resolution, {
await expect(
fetch.tarball(cafs, resolution, {
lockfileDir: process.cwd(),
})
t.fail('should have failed')
} catch (err) {
t.equal(err.message, 'Got unexpected checksum for "http://example.com/foo.tgz". Wanted "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=". ' +
'Got "sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=".')
t.equal(err['code'], 'ERR_PNPM_TARBALL_INTEGRITY')
t.equal(err['resource'], 'http://example.com/foo.tgz')
t.equal(err['attempts'], 2)
t.ok(scope.isDone())
t.end()
}
).rejects.toThrow(
new TarballIntegrityError({
algorithm: 'sha512',
expected: 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY=',
found: 'sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=',
sri: '',
url: resolution.tarball,
})
)
expect(scope.isDone()).toBeTruthy()
})
test('retry when integrity check fails', async t => {
test('retry when integrity check fails', async () => {
const scope = nock(registry)
.get('/foo.tgz')
.replyWithFile(200, path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz'), {
@@ -137,7 +131,6 @@ test('retry when integrity check fails', async t => {
})
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const resolution = {
integrity: tarballIntegrity,
@@ -152,16 +145,15 @@ test('retry when integrity check fails', async t => {
},
})
t.deepEqual(params[0], [1194, 1])
t.deepEqual(params[1], [tarballSize, 2])
expect(params[0]).toStrictEqual([1194, 1])
expect(params[1]).toStrictEqual([tarballSize, 2])
t.ok(scope.isDone())
t.end()
expect(scope.isDone()).toBeTruthy()
})
test('fail when integrity check of local file fails', async (t) => {
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
test('fail when integrity check of local file fails', async () => {
const storeDir = tempy.directory()
process.chdir(storeDir)
await cpFile(
path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz'),
@@ -172,28 +164,23 @@ test('fail when integrity check of local file fails', async (t) => {
tarball: 'file:tar.tgz',
}
let err: Error | null = null
try {
await fetch.tarball(cafs, resolution, {
await expect(
fetch.tarball(cafs, resolution, {
lockfileDir: process.cwd(),
})
} catch (_err) {
err = _err
}
t.ok(err, 'error thrown')
t.equal(err.message, 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY= integrity checksum failed when using sha1: ' +
'wanted sha1-HssnaJydJVE+rbyZFKc/VAi+enY= but got sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=. (1194 bytes)')
t.equal(err['code'], 'EINTEGRITY')
t.equal(err['resource'], path.resolve('tar.tgz'))
t.equal(err['attempts'], 1)
t.end()
).rejects.toThrow(
new TarballIntegrityError({
algorithm: 'sha512',
expected: 'sha1-HssnaJydJVE+rbyZFKc/VAi+enY=',
found: 'sha512-VuFL1iPaIxJK/k3gTxStIkc6+wSiDwlLdnCWNZyapsVLobu/0onvGOZolASZpfBFiDJYrOIGiDzgLIULTW61Vg== sha1-ACjKMFA7S6uRFXSDFfH4aT+4B4Y=',
sri: '',
url: path.join(storeDir, 'tar.tgz'),
})
)
})
test("don't fail when integrity check of local file succeeds", async (t) => {
test("don't fail when integrity check of local file succeeds", async () => {
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const localTarballLocation = path.resolve('tar.tgz')
await cpFile(
@@ -209,14 +196,11 @@ test("don't fail when integrity check of local file succeeds", async (t) => {
lockfileDir: process.cwd(),
})
t.equal(typeof filesIndex['package.json'], 'object', 'files index returned')
t.end()
expect(typeof filesIndex['package.json']).toBe('object')
})
test("don't fail when fetching a local tarball in offline mode", async (t) => {
test("don't fail when fetching a local tarball in offline mode", async () => {
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const tarballAbsoluteLocation = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz')
const resolution = {
@@ -236,14 +220,11 @@ test("don't fail when fetching a local tarball in offline mode", async (t) => {
lockfileDir: process.cwd(),
})
t.equal(typeof filesIndex['package.json'], 'object', 'files index returned')
t.end()
expect(typeof filesIndex['package.json']).toBe('object')
})
test('fail when trying to fetch a non-local tarball in offline mode', async (t) => {
test('fail when trying to fetch a non-local tarball in offline mode', async () => {
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const tarballAbsoluteLocation = path.join(__dirname, 'tars', 'babel-helper-hoist-variables-7.0.0-alpha.10.tgz')
const resolution = {
@@ -251,30 +232,26 @@ test('fail when trying to fetch a non-local tarball in offline mode', async (t)
tarball: `${registry}foo.tgz`,
}
let err!: Error
try {
const fetch = createFetcher(fetchFromRegistry, getCredentials, {
offline: true,
retry: {
maxTimeout: 100,
minTimeout: 0,
retries: 1,
},
})
await fetch.tarball(cafs, resolution, {
const fetch = createFetcher(fetchFromRegistry, getCredentials, {
offline: true,
retry: {
maxTimeout: 100,
minTimeout: 0,
retries: 1,
},
})
await expect(
fetch.tarball(cafs, resolution, {
lockfileDir: process.cwd(),
})
} catch (_err) {
err = _err
}
t.ok(err)
t.equal(err['code'], 'ERR_PNPM_NO_OFFLINE_TARBALL')
t.end()
).rejects.toThrow(
new PnpmError('NO_OFFLINE_TARBALL',
`A package is missing from the store but cannot download it in offline mode. \
The missing package may be downloaded from ${resolution.tarball}.`)
)
})
test('retry on server error', async t => {
test('retry on server error', async () => {
const scope = nock(registry)
.get('/foo.tgz')
.reply(500)
@@ -284,7 +261,6 @@ test('retry on server error', async t => {
})
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const resolution = {
integrity: tarballIntegrity,
@@ -295,47 +271,42 @@ test('retry on server error', async t => {
lockfileDir: process.cwd(),
})
t.ok(index)
expect(index).toBeTruthy()
t.ok(scope.isDone())
t.end()
expect(scope.isDone()).toBeTruthy()
})
test('throw error when accessing private package w/o authorization', async t => {
test('throw error when accessing private package w/o authorization', async () => {
const scope = nock(registry)
.get('/foo.tgz')
.reply(403)
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const resolution = {
integrity: tarballIntegrity,
tarball: 'http://example.com/foo.tgz',
}
let err!: Error
try {
await fetch.tarball(cafs, resolution, {
await expect(
fetch.tarball(cafs, resolution, {
lockfileDir: process.cwd(),
})
} catch (_err) {
err = _err
}
t.ok(err)
err = err || new Error()
t.equal(err.message, 'GET http://example.com/foo.tgz: Forbidden - 403')
t.equal(err['hint'], 'No authorization header was set for the request.')
t.equal(err['code'], 'ERR_PNPM_FETCH_403')
t.equal(err['request']['url'], 'http://example.com/foo.tgz')
t.ok(scope.isDone())
t.end()
).rejects.toThrow(
new FetchError(
{
url: resolution.tarball,
},
{
status: 403,
statusText: 'Forbidden',
}
)
)
expect(scope.isDone()).toBeTruthy()
})
test('accessing private packages', async t => {
test('accessing private packages', async () => {
const scope = nock(
registry,
{
@@ -350,7 +321,6 @@ test('accessing private packages', async t => {
})
process.chdir(tempy.directory())
t.comment(`testing in ${process.cwd()}`)
const getCredentials = () => ({
alwaysAuth: undefined,
@@ -374,10 +344,9 @@ test('accessing private packages', async t => {
lockfileDir: process.cwd(),
})
t.ok(index)
expect(index).toBeTruthy()
t.ok(scope.isDone())
t.end()
expect(scope.isDone()).toBeTruthy()
})
async function getFileIntegrity (filename: string) {

View File

@@ -1,2 +0,0 @@
/// <reference path="../../../typings/index.d.ts"/>
import './download'