318 lines
8.0 KiB
JavaScript
318 lines
8.0 KiB
JavaScript
'use strict'
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const ssri = require('ssri')
|
|
const t = require('tap')
|
|
|
|
const CacheContent = require('../fixtures/cache-content')
|
|
|
|
const read = require('../../lib/content/read')
|
|
|
|
// defines reusable errors
|
|
const genericError = new Error('ERR')
|
|
genericError.code = 'ERR'
|
|
const permissionError = new Error('EPERM')
|
|
permissionError.code = 'EPERM'
|
|
|
|
// helpers
|
|
const getRead = (t, opts) => t.mock('../../lib/content/read', opts)
|
|
const getReadStatFailure = (t, err) => getRead(t, {
|
|
fs: {
|
|
...fs,
|
|
statSync: () => {
|
|
throw err
|
|
},
|
|
},
|
|
'fs/promises': {
|
|
...fs.promises,
|
|
stat: async (path) => {
|
|
throw err
|
|
},
|
|
},
|
|
})
|
|
|
|
t.test('read: returns a Promise with cache content data', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT,
|
|
})
|
|
)
|
|
const data = await read(CACHE, INTEGRITY)
|
|
t.same(data, CONTENT, 'cache contents read correctly')
|
|
})
|
|
|
|
t.test('read.stream: returns a stream with cache content data', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT,
|
|
})
|
|
)
|
|
const stream = read.stream(CACHE, INTEGRITY)
|
|
const [fromStream, fromBulk] = await Promise.all([
|
|
stream.concat(),
|
|
read(CACHE, INTEGRITY, { size: CONTENT.length }),
|
|
])
|
|
t.same(fromStream, CONTENT, 'stream data checks out')
|
|
t.same(fromBulk, CONTENT, 'promise data checks out')
|
|
})
|
|
|
|
t.test('read: allows hashAlgorithm configuration', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const HASH = 'sha384'
|
|
const INTEGRITY = ssri.fromData(CONTENT, { algorithms: [HASH] })
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT,
|
|
})
|
|
)
|
|
const stream = read.stream(CACHE, INTEGRITY)
|
|
const [fromStream, fromBulk] = await Promise.all([
|
|
stream.concat(),
|
|
read(CACHE, INTEGRITY),
|
|
])
|
|
t.same(fromStream, CONTENT, 'stream used algorithm')
|
|
t.same(fromBulk, CONTENT, 'promise used algorithm')
|
|
})
|
|
|
|
t.test('read: errors if content missing', async t => {
|
|
const CACHE = t.testdir({})
|
|
const stream = read.stream(CACHE, 'sha512-whatnot')
|
|
stream.on('data', function (data) {
|
|
throw new Error('unexpected data: ' + JSON.stringify(data))
|
|
})
|
|
stream.on('end', function () {
|
|
throw new Error('end was emitted even though stream errored')
|
|
})
|
|
await t.rejects(
|
|
stream.promise(),
|
|
{ code: 'ENOENT' },
|
|
'stream got the right error'
|
|
)
|
|
await t.rejects(
|
|
read(CACHE, 'sha512-whatnot'),
|
|
{ code: 'ENOENT' },
|
|
'bulk got the right error'
|
|
)
|
|
})
|
|
|
|
t.test('read: errors if content fails checksum', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT.slice(3), // invalid contents!
|
|
})
|
|
)
|
|
const stream = read.readStream(CACHE, INTEGRITY)
|
|
stream.on('end', function () {
|
|
throw new Error('end was emitted even though stream errored')
|
|
})
|
|
await t.rejects(
|
|
stream.promise(),
|
|
{ code: 'EINTEGRITY' },
|
|
'stream got the right error'
|
|
)
|
|
await t.rejects(
|
|
read(CACHE, INTEGRITY),
|
|
{ code: 'EINTEGRITY' },
|
|
'bulk got the right error'
|
|
)
|
|
})
|
|
|
|
t.test('read: errors if content size does not match size option', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT.slice(3), // invalid contents!
|
|
})
|
|
)
|
|
const stream = read.readStream(CACHE, INTEGRITY, { size: CONTENT.length })
|
|
stream.on('end', function () {
|
|
throw new Error('end was called even though stream errored')
|
|
})
|
|
await t.rejects(
|
|
stream.promise(),
|
|
{ code: 'EBADSIZE' },
|
|
'stream got the right error'
|
|
)
|
|
await t.rejects(
|
|
read(CACHE, INTEGRITY, { size: CONTENT.length }),
|
|
{ code: 'EBADSIZE' },
|
|
'bulk got the right error'
|
|
)
|
|
})
|
|
|
|
t.test('read: error while parsing provided integrity data', function (t) {
|
|
const CACHE = t.testdir()
|
|
const INTEGRITY = 'sha1-deadbeef'
|
|
const mockedRead = getRead(t, {
|
|
ssri: {
|
|
parse (sri) {
|
|
throw genericError
|
|
},
|
|
},
|
|
})
|
|
|
|
t.plan(1)
|
|
return t.rejects(
|
|
mockedRead(CACHE, INTEGRITY),
|
|
genericError,
|
|
'should reject promise upon catching internal errors'
|
|
)
|
|
})
|
|
|
|
t.test('read: unknown error parsing nested integrity data', function (t) {
|
|
const CACHE = t.testdir()
|
|
const INTEGRITY = 'sha1-deadbeef sha1-13371337'
|
|
|
|
// patches method in order to force a last error scenario
|
|
const mockedRead = getRead(t, {
|
|
ssri: {
|
|
parse (sri) {
|
|
if (sri !== INTEGRITY) {
|
|
throw genericError
|
|
}
|
|
|
|
return ssri.parse(sri)
|
|
},
|
|
},
|
|
})
|
|
|
|
t.plan(1)
|
|
return t.rejects(
|
|
mockedRead(CACHE, INTEGRITY),
|
|
genericError,
|
|
'should throw unknown errors'
|
|
)
|
|
})
|
|
|
|
t.test('read: returns only first result if other hashes fails', function (t) {
|
|
// sets up a cache that has multiple entries under the
|
|
// same algorithm but then only one has real contents in the fs
|
|
const CONTENT = {
|
|
foo: Buffer.from('foo'),
|
|
bar: Buffer.from('bar'),
|
|
}
|
|
const INTEGRITY = ssri.fromData(CONTENT.foo).concat(
|
|
ssri.fromData(CONTENT.bar)
|
|
)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY.sha512[1]]: CONTENT.bar,
|
|
})
|
|
)
|
|
|
|
t.plan(1)
|
|
return t.resolveMatch(
|
|
read(CACHE, INTEGRITY),
|
|
CONTENT.bar,
|
|
'should return only the first valid result'
|
|
)
|
|
})
|
|
|
|
t.test('read: opening large files', function (t) {
|
|
const CACHE = t.testdir()
|
|
const mockedRead = getRead(t, {
|
|
'fs/promises': {
|
|
...fs.promises,
|
|
stat: async (path) => {
|
|
return { size: Number.MAX_SAFE_INTEGER }
|
|
},
|
|
},
|
|
'fs-minipass': {
|
|
ReadStream: class {
|
|
constructor (path, opts) {
|
|
t.match(
|
|
opts,
|
|
{
|
|
readSize: 64 * 1024 * 1024,
|
|
size: Number.MAX_SAFE_INTEGER,
|
|
},
|
|
'should use fs-minipass interface'
|
|
)
|
|
}
|
|
},
|
|
},
|
|
'minipass-pipeline': Array,
|
|
})
|
|
|
|
t.plan(1)
|
|
mockedRead(CACHE, 'sha1-deadbeef')
|
|
})
|
|
|
|
t.test('hasContent: tests content existence', async t => {
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
'sha1-deadbeef': '',
|
|
})
|
|
)
|
|
const content = await read.hasContent(CACHE, 'sha1-deadbeef')
|
|
t.ok(content.sri, 'returned sri for this content')
|
|
t.equal(content.size, 0, 'returned the right size for this content')
|
|
t.ok(content.stat.isFile(), 'returned actual stat object')
|
|
await t.resolveMatch(
|
|
read.hasContent(CACHE, 'sha1-not-there'),
|
|
false,
|
|
'returned false for missing content'
|
|
)
|
|
await t.resolveMatch(
|
|
read.hasContent(CACHE, 'sha1-not-here sha1-also-not-here'),
|
|
false,
|
|
'multi-content hash failures work ok'
|
|
)
|
|
})
|
|
|
|
t.test('hasContent: permission error', (t) => {
|
|
const CACHE = t.testdir()
|
|
// setup a syntetic permission error
|
|
const mockedRead = getReadStatFailure(t, permissionError)
|
|
|
|
t.plan(1)
|
|
t.rejects(
|
|
mockedRead.hasContent(CACHE, 'sha1-deadbeef sha1-13371337'),
|
|
permissionError,
|
|
'should reject on permission errors'
|
|
)
|
|
})
|
|
|
|
t.test('hasContent: generic error', (t) => {
|
|
const CACHE = t.testdir()
|
|
const mockedRead = getReadStatFailure(t, genericError)
|
|
|
|
t.plan(1)
|
|
t.resolves(
|
|
mockedRead.hasContent(CACHE, 'sha1-deadbeef sha1-13371337'),
|
|
'should not reject on generic errors'
|
|
)
|
|
})
|
|
|
|
t.test('hasContent: no integrity provided', (t) => {
|
|
const CACHE = t.testdir()
|
|
t.resolveMatch(
|
|
read.hasContent(CACHE, ''),
|
|
false,
|
|
'should resolve with a value of false'
|
|
)
|
|
t.end()
|
|
})
|
|
|
|
t.test('copy: copies content to a destination path', async t => {
|
|
const CONTENT = Buffer.from('foobarbaz')
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT,
|
|
})
|
|
)
|
|
const DEST = path.join(CACHE, 'foobar-file')
|
|
await read.copy(CACHE, INTEGRITY, DEST)
|
|
const data = await fs.promises.readFile(DEST)
|
|
t.same(data, CONTENT, 'file successfully copied')
|
|
})
|