341 lines
10 KiB
JavaScript
341 lines
10 KiB
JavaScript
'use strict'
|
|
|
|
const events = require('events')
|
|
const fs = require('fs')
|
|
const Minipass = require('minipass')
|
|
const path = require('path')
|
|
const ssri = require('ssri')
|
|
const t = require('tap')
|
|
|
|
const CacheContent = require('../fixtures/cache-content')
|
|
const contentPath = require('../../lib/content/path')
|
|
|
|
const write = require('../../lib/content/write')
|
|
|
|
t.test('basic put', async t => {
|
|
const CACHE = t.testdir()
|
|
const CONTENT = 'foobarbaz'
|
|
// Default is sha512
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
let integrity
|
|
await write.stream(CACHE).on('integrity', (i) => {
|
|
integrity = i
|
|
}).end(CONTENT).promise()
|
|
const cpath = contentPath(CACHE, integrity)
|
|
t.same(integrity, INTEGRITY, 'calculated integrity value matches')
|
|
t.ok(fs.lstatSync(cpath).isFile(), 'content inserted as a single file')
|
|
t.equal(fs.readFileSync(cpath, 'utf8'), CONTENT, 'contents are identical to inserted content')
|
|
})
|
|
|
|
t.test('basic put, providing external integrity emitter', async (t) => {
|
|
const CACHE = t.testdir()
|
|
const CONTENT = 'foobarbaz'
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
|
|
const write = t.mock('../../lib/content/write.js', {
|
|
ssri: {
|
|
...ssri,
|
|
integrityStream: () => {
|
|
throw new Error('Should not be called')
|
|
},
|
|
},
|
|
})
|
|
|
|
const source = new Minipass().end(CONTENT)
|
|
|
|
const tee = new Minipass()
|
|
|
|
const integrityStream = ssri.integrityStream()
|
|
// since the integrityStream is not going anywhere, we need to manually resume it
|
|
// otherwise it'll get stuck in paused mode and will never process any data events
|
|
integrityStream.resume()
|
|
const integrityStreamP = Promise.all([
|
|
events.once(integrityStream, 'integrity').then((res) => res[0]),
|
|
events.once(integrityStream, 'size').then((res) => res[0]),
|
|
])
|
|
|
|
const contentStream = write.stream(CACHE, { integrityEmitter: integrityStream })
|
|
const contentStreamP = Promise.all([
|
|
events.once(contentStream, 'integrity').then((res) => res[0]),
|
|
events.once(contentStream, 'size').then((res) => res[0]),
|
|
contentStream.promise(),
|
|
])
|
|
|
|
tee.pipe(integrityStream)
|
|
tee.pipe(contentStream)
|
|
source.pipe(tee)
|
|
|
|
const [
|
|
[ssriIntegrity, ssriSize],
|
|
[contentIntegrity, contentSize],
|
|
] = await Promise.all([
|
|
integrityStreamP,
|
|
contentStreamP,
|
|
])
|
|
|
|
t.equal(ssriSize, CONTENT.length, 'ssri got the right size')
|
|
t.equal(contentSize, CONTENT.length, 'content got the right size')
|
|
t.same(ssriIntegrity, INTEGRITY, 'ssri got the right integrity')
|
|
t.same(contentIntegrity, INTEGRITY, 'content got the right integrity')
|
|
|
|
const cpath = contentPath(CACHE, ssriIntegrity)
|
|
t.ok(fs.lstatSync(cpath).isFile(), 'content inserted as a single file')
|
|
t.equal(fs.readFileSync(cpath, 'utf8'), CONTENT, 'contents are identical to inserted content')
|
|
})
|
|
|
|
t.test("checks input digest doesn't match data", async t => {
|
|
const CONTENT = 'foobarbaz'
|
|
const integrity = ssri.fromData(CONTENT)
|
|
let int1 = null
|
|
let int2 = null
|
|
const CACHE = t.testdir()
|
|
|
|
await t.rejects(
|
|
write.stream(CACHE, { integrity }).on('integrity', (int) => {
|
|
int1 = int
|
|
})
|
|
.end('bazbarfoo').promise(),
|
|
{ code: 'EINTEGRITY' },
|
|
'returns integrity error'
|
|
)
|
|
t.equal(int1, null, 'no digest emitted')
|
|
await write.stream(CACHE, { integrity }).on('integrity', int => {
|
|
int2 = int
|
|
})
|
|
.end(CONTENT).promise()
|
|
t.same(int2, integrity, 'returns a matching digest')
|
|
})
|
|
|
|
t.test('errors if stream ends with no data', async t => {
|
|
const CACHE = t.testdir()
|
|
let integrity = null
|
|
await t.rejects(
|
|
write.stream(CACHE).end('').on('integrity', int => {
|
|
integrity = int
|
|
}).promise(),
|
|
{ code: 'ENODATA' },
|
|
'get an error with a useful code'
|
|
)
|
|
t.equal(integrity, null, 'no digest returned')
|
|
})
|
|
|
|
t.test('errors if input size does not match expected', async t => {
|
|
let int1 = null
|
|
let int2 = null
|
|
|
|
const CACHE = t.testdir()
|
|
await t.rejects(
|
|
write.stream(CACHE, { size: 5 }).on('integrity', int => {
|
|
int1 = int
|
|
}).end('abc').promise(),
|
|
{ code: 'EBADSIZE', expected: 5, found: 3 },
|
|
'get an error when data smaller than expected'
|
|
)
|
|
t.equal(int1, null, 'no digest returned')
|
|
await t.rejects(
|
|
write.stream(CACHE, { size: 5 }).on('integrity', int => {
|
|
int2 = int
|
|
}).end('abcdefghi').promise(),
|
|
{ code: 'EBADSIZE', expected: 5, found: 9 },
|
|
'get an error when data bigger than expected'
|
|
)
|
|
t.equal(int2, null, 'no digest returned')
|
|
})
|
|
|
|
t.test('does not overwrite content if already on disk', async t => {
|
|
const CONTENT = 'foobarbaz'
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: 'nope',
|
|
})
|
|
)
|
|
|
|
let int1
|
|
let int2
|
|
// With a digest -- early short-circuiting
|
|
await write.stream(CACHE, { integrity: INTEGRITY }).on('integrity', int => {
|
|
int1 = int
|
|
})
|
|
.end(CONTENT).promise()
|
|
t.same(int1, INTEGRITY, 'short-circuit returns a matching digest')
|
|
const d1 = fs.readFileSync(contentPath(CACHE, INTEGRITY), 'utf8')
|
|
t.equal(d1, 'nope', 'process short-circuited. Data not written.')
|
|
await write.stream(CACHE).on('integrity', int => {
|
|
int2 = int
|
|
})
|
|
.end(CONTENT).promise()
|
|
t.same(int2, INTEGRITY, 'full write returns a matching digest')
|
|
const d2 = fs.readFileSync(contentPath(CACHE, INTEGRITY), 'utf8')
|
|
t.equal(d2, 'nope', 'previously-written data intact - no dupe write')
|
|
})
|
|
|
|
t.test('errors if input stream errors', async t => {
|
|
let integrity = null
|
|
const CACHE = t.testdir()
|
|
const putter = write.stream(CACHE)
|
|
.on('integrity', (int) => {
|
|
integrity = int
|
|
})
|
|
setTimeout(() => putter.inputStream.emit('error', new Error('bleh')))
|
|
await t.rejects(putter.promise(), { message: 'bleh' })
|
|
t.equal(integrity, null, 'no digest returned')
|
|
t.throws(() => {
|
|
fs.statSync(contentPath(CACHE, ssri.fromData('foobarbaz')))
|
|
}, {
|
|
code: 'ENOENT',
|
|
}, 'target file missing. No files created')
|
|
})
|
|
|
|
t.test('exits normally if file already open', (t) => {
|
|
const CONTENT = 'foobarbaz'
|
|
const INTEGRITY = ssri.fromData(CONTENT)
|
|
const CACHE = t.testdir(
|
|
CacheContent({
|
|
[INTEGRITY]: CONTENT,
|
|
})
|
|
)
|
|
let integrity
|
|
// This case would only fail on Windows, when an entry is being read.
|
|
// Generally, you'd get an EBUSY back.
|
|
fs.open(contentPath(CACHE, INTEGRITY), 'r+', async function (err, fd) {
|
|
if (err) {
|
|
throw err
|
|
}
|
|
|
|
await write.stream(CACHE).on('integrity', int => {
|
|
integrity = int
|
|
})
|
|
.end(CONTENT)
|
|
.promise()
|
|
t.same(integrity, INTEGRITY, 'returns a matching digest')
|
|
fs.closeSync(fd)
|
|
fs.rmSync(contentPath(CACHE, INTEGRITY), { recursive: true, force: true })
|
|
t.end()
|
|
})
|
|
})
|
|
|
|
t.test('cleans up tmp on successful completion', async t => {
|
|
const CONTENT = 'foobarbaz'
|
|
const CACHE = t.testdir()
|
|
await write.stream(CACHE).end(CONTENT).promise()
|
|
await new Promise((resolve, reject) => {
|
|
const tmp = path.join(CACHE, 'tmp')
|
|
fs.readdir(tmp, function (err, files) {
|
|
if (!err || (err && err.code === 'ENOENT')) {
|
|
files = files || []
|
|
t.same(files, [], 'nothing in the tmp dir!')
|
|
resolve()
|
|
} else {
|
|
reject(err)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
t.test('cleans up tmp on streaming error', (t) => {
|
|
const CONTENT = 'foobarbaz'
|
|
const CACHE = t.testdir()
|
|
return t.rejects(
|
|
write.stream(CACHE, { size: 1 })
|
|
.end(CONTENT)
|
|
.promise(),
|
|
{ code: 'EBADSIZE' },
|
|
'got expected code'
|
|
)
|
|
.then(() => new Promise((resolve, reject) => {
|
|
const tmp = path.join(CACHE, 'tmp')
|
|
fs.readdir(tmp, function (err, files) {
|
|
if (!err || (err && err.code === 'ENOENT')) {
|
|
files = files || []
|
|
t.same(files, [], 'nothing in the tmp dir!')
|
|
resolve()
|
|
} else {
|
|
reject(err)
|
|
}
|
|
})
|
|
}))
|
|
})
|
|
|
|
t.test('cleans up tmp on non streaming error', (t) => {
|
|
// mock writefile and make it reject
|
|
const CONTENT = 'foobarbaz'
|
|
const CACHE = t.testdir({ 'content-v2': 'oh no a file' })
|
|
return t.rejects(write(CACHE, CONTENT))
|
|
.then(() => new Promise((resolve, reject) => {
|
|
const tmp = path.join(CACHE, 'tmp')
|
|
fs.readdir(tmp, function (err, files) {
|
|
if (!err || (err && err.code === 'ENOENT')) {
|
|
files = files || []
|
|
t.same(files, [], 'nothing in the tmp dir!')
|
|
resolve()
|
|
} else {
|
|
reject(err)
|
|
}
|
|
})
|
|
}))
|
|
})
|
|
|
|
t.test('checks the size of stream data if opts.size provided', (t) => {
|
|
const CONTENT = 'foobarbaz'
|
|
let int1 = null
|
|
const int2 = null
|
|
let int3 = null
|
|
|
|
const CACHE = t.testdir()
|
|
t.test('chair too small', t => {
|
|
const w = write.stream(CACHE, { size: CONTENT.length })
|
|
w.write(CONTENT.slice(3))
|
|
w.on('integrity', int => {
|
|
int1 = int
|
|
})
|
|
setTimeout(() => w.end())
|
|
return t.rejects(w.promise(), { code: 'EBADSIZE' }, 'bad size error code')
|
|
.then(() => t.equal(int1, null, 'no digest returned by first stream'))
|
|
})
|
|
|
|
t.test('chair is too big', t => {
|
|
const w = write.stream(CACHE, { size: CONTENT.length })
|
|
w.write(CONTENT)
|
|
setTimeout(() => w.end('quux'))
|
|
return t.rejects(w.promise(), { code: 'EBADSIZE' }, 'bad size error code')
|
|
.then(() => t.equal(int2, null, 'no digest returned by second stream'))
|
|
})
|
|
|
|
return t.test('chair is juuuuust right', t => {
|
|
const w = write.stream(CACHE, { size: CONTENT.length })
|
|
w.write(CONTENT)
|
|
w.on('integrity', int => {
|
|
int3 = int
|
|
})
|
|
setTimeout(() => w.end())
|
|
return w.promise().then(() => t.ok(int3, 'got a digest'))
|
|
})
|
|
})
|
|
|
|
t.test('only one algorithm for now', async t => {
|
|
const CACHE = t.testdir()
|
|
await t.rejects(() => write(CACHE, 'foo', { algorithms: [1, 2] }), {
|
|
message: 'opts.algorithms only supports a single algorithm for now',
|
|
})
|
|
})
|
|
|
|
t.test('writes to cache with default options', t => {
|
|
const CACHE = t.testdir()
|
|
return t.resolveMatch(write(CACHE, 'foo'), {
|
|
size: 3,
|
|
integrity: {
|
|
sha512: [
|
|
{
|
|
/* eslint-disable-next-line max-len */
|
|
source: 'sha512-9/u6bgY2+JDlb7vzKD5STG+jIErimDgtYkdB0NxmODJuKCxBvl5CVNiCB3LFUYosWowMf37aGVlKfrU5RT4e1w==',
|
|
/* eslint-disable-next-line max-len */
|
|
digest: '9/u6bgY2+JDlb7vzKD5STG+jIErimDgtYkdB0NxmODJuKCxBvl5CVNiCB3LFUYosWowMf37aGVlKfrU5RT4e1w==',
|
|
algorithm: 'sha512',
|
|
options: [],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
})
|