node-cacache/test/content/read.js

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')
})