import { Eltro as t, assert, spy } from 'eltro' import path from 'path' import crypto from 'crypto' import os from 'os' import fsSyncOriginal from 'fs' import fsPromisesOriginal from 'fs/promises' import Cache from '../index.mjs' import { fakeFs, fakeFsPromises, fakeFsSync } from './helper.mjs' let fsSync let fsPromises t.beforeEach(function() { fsSync = fakeFsSync() fsPromises = fakeFsPromises() }) function createCache(opts) { return new Cache(opts, fsSync, fsPromises) } t.describe('#constructor()', function() { t.test('uses default fs', function() { let cache = new Cache({}) assert.strictEqual(cache.fsSync, fsSyncOriginal) assert.strictEqual(cache.fsPromises, fsPromisesOriginal) }) t.test('should be able to override the fs', function() { let cache = createCache({}) assert.strictEqual(cache.fsSync, fsSync) assert.strictEqual(cache.fsPromises, fsPromises) }) t.test('comes with default options', function() { let cache = createCache({}) assert.ok(cache.id) assert.strictEqual(cache.ttl, 0) assert.strictEqual(cache.prefix, '-') assert.strictEqual(cache.hash_alg, 'md5') assert.strictEqual(cache.cache_dir, path.join(os.tmpdir(), cache.id)) }) t.test('can overwrite options', function() { const assertHash = 'sha256' const assertDir = '/something/else' const assertPrefix = 'blabla' const assertTtl = 60 let cache = createCache({ prefix: assertPrefix, hash_alg: assertHash, cache_dir: assertDir, ttl: assertTtl, }) assert.ok(cache.id) assert.strictEqual(cache.prefix, assertPrefix + '-') assert.strictEqual(cache.hash_alg, assertHash) assert.strictEqual(cache.cache_dir, assertDir) assert.strictEqual(cache.ttl, assertTtl) }) t.test('should create the directory by default', function() { assert.notOk(fsSync.mkdirSync.called) let cache = createCache({}) assert.ok(fsSync.mkdirSync.called) assert.strictEqual(fsSync.mkdirSync.firstCall[0], cache.cache_dir) assert.strictEqual(fsSync.mkdirSync.firstCall[1]?.recursive, true) }) t.test('should check if hash_alg is valid', function() { assert.throws(function() { createCache({ hash_alg: 'dafdsagasdgwa4e' }) }) }) }) t.describe('FSCache', function() { t.describe('#hash()', function() { t.test('should use cache hasher to hash string', function() { let cache = createCache({ hash_alg: 'sha256' }) assert.strictEqual(cache.hash('asdf'), path.join(cache.cache_dir, '-' + crypto.hash('sha256', 'asdf'))) cache = createCache({ hash_alg: 'md5' }) assert.strictEqual(cache.hash('asdf'), path.join(cache.cache_dir, '-' + crypto.hash('md5', 'asdf'))) }) t.test('should add prefix if prefix is defined', function() { let cache = createCache({ prefix: 'asdfg', hash_alg: 'md5' }) assert.strictEqual(cache.hash('asdf'), path.join(cache.cache_dir, 'asdfg-' + crypto.hash('md5', 'asdf'))) }) }) t.describe('#_parseCacheData()', function() { t.test('should default parse as json', function() { let cache = createCache() let output = cache._parseCacheData('{"content":{"hello":"world"}}') assert.strictEqual(typeof output, 'object') assert.strictEqual(output.hello, 'world') }) t.test('should return content if not expired', function() { let expiredTime = new Date().getTime() + 1000 let cache = createCache() let output = cache._parseCacheData(`{"content":{"hello":"world"},"ttl":${expiredTime}}`) assert.strictEqual(typeof output, 'object') assert.strictEqual(output.hello, 'world') }) t.test('should return null if ttl expired', function() { let expiredTime = new Date().getTime() - 1 let cache = createCache() let output = cache._parseCacheData(`{"content":{"hello":"world"},"ttl":${expiredTime}}`) assert.strictEqual(output, null) }) t.test('should fall back to fallback if ttl expired', function() { const fallback = { a: 1 } let cache = createCache() let output = cache._parseCacheData('{"content":{"hello":"world"},"ttl":0}', fallback) assert.strictEqual(output, fallback) }) }) t.describe('#_parseSetData()', function() { t.test('should default stringify to json', function() { const assertKey = 'mytestkey-1234' let cache = createCache() let output = cache._parseSetData(assertKey, { hello: 'world' }) assert.strictEqual(typeof output, 'string') assert.strictEqual(output, `{"key":"${assertKey}","content":{"hello":"world"}}`) }) t.test('should include ttl if specified in options', function() { const assertData = { a: 1 } let cache = createCache({ ttl: 60 }) let output = cache._parseSetData('a', assertData) let back = JSON.parse(output) assert.ok(back.ttl) assert.deepStrictEqual(back.content, assertData) assert.equalWithMargin(new Date().getTime() + 60 * 1000, back.ttl, 1000) }) t.test('should include ttl if specified in parameters', function() { const assertData = { a: 1 } const assertKey = 'mytestkey-1234' let cache = createCache() let output = cache._parseSetData(assertKey, assertData, { ttl: 60 }) let back = JSON.parse(output) assert.ok(back.ttl) assert.strictEqual(back.key, assertKey) assert.deepStrictEqual(back.content, assertData) assert.equalWithMargin(new Date().getTime() + 60 * 1000, back.ttl, 1000) }) t.test('ttl in parameter should overwrite options', function() { const assertData = { a: 1 } let cache = createCache({ ttl: 30 }) let output = cache._parseSetData('a', assertData, { ttl: 60 }) let back = JSON.parse(output) assert.ok(back.ttl) assert.deepStrictEqual(back.content, assertData) assert.equalWithMargin(new Date().getTime() + 60 * 1000, back.ttl, 1000) }) t.test('ttl in parameter with 0 should overwrite options with no ttl', function() { const assertData = { a: 1 } let cache = createCache({ ttl: 30 }) let output = cache._parseSetData('a', assertData, { ttl: 0 }) let back = JSON.parse(output) assert.notOk(back.ttl) }) }) t.describe('#get()', function() { t.test('should call promise readFile and parse result', async function() { const assertKey = 'asdf1234' const assertContent = '{"hello":"world"}' const assertResult = { hello: 'world' } let cache = createCache() fsPromises.readFile.resolves(assertContent) cache._parseCacheData = spy().returns(assertResult) let output = await cache.get(assertKey) assert.strictEqual(output, assertResult) assert.ok(fsPromises.readFile.called) assert.strictEqual(fsPromises.readFile.firstCall[0], cache.hash(assertKey)) assert.strictEqual(fsPromises.readFile.firstCall[1]?.encoding, 'utf8') assert.ok(cache._parseCacheData.called) assert.ok(cache._parseCacheData.firstCall[0], assertContent) }) t.test('should pass extra options and fallback to the parser', async function() { const assertOptions = { a: 1 } const assertFallback = { b: 2 } let cache = createCache() cache._parseCacheData = spy() await cache.get('asdf', assertFallback, assertOptions) assert.ok(cache._parseCacheData.called) assert.ok(cache._parseCacheData.firstCall[1], assertFallback) assert.ok(cache._parseCacheData.firstCall[2], assertOptions) }) t.test('should support fallback value if file does not exist', async function() { const assertFallback = { a: 1 } let cache = createCache() fsPromises.readFile.rejects(new Error('asdf')) cache._parseCacheData = spy() let output = await cache.get('bla', assertFallback) assert.strictEqual(output, assertFallback) assert.notOk(cache._parseCacheData.called) }) t.test('parser error should propogate', async function() { const assertError = new Error('Hello') let cache = createCache() cache._parseCacheData = spy().throws(assertError) let err = await assert.isRejected(cache.get('asdf')) assert.strictEqual(err, assertError) }) }) t.describe('#getSync()', function() { t.test('should call sync readFile and parse result', function() { const assertKey = 'asdf1234' const assertContent = '{"hello":"world"}' const assertResult = { hello: 'world' } let cache = createCache() fsSync.readFileSync.returns(assertContent) cache._parseCacheData = spy().returns(assertResult) let output = cache.getSync(assertKey) assert.strictEqual(output, assertResult) assert.ok(fsSync.readFileSync.called) assert.strictEqual(fsSync.readFileSync.firstCall[0], cache.hash(assertKey)) assert.strictEqual(fsSync.readFileSync.firstCall[1]?.encoding, 'utf8') assert.ok(cache._parseCacheData.called) assert.ok(cache._parseCacheData.firstCall[0], assertContent) }) t.test('should pass extra options to the parser', function() { const assertOptions = { a: 1 } const assertFallback = { b: 2 } let cache = createCache() cache._parseCacheData = spy() cache.getSync('asdf', assertFallback, assertOptions) assert.ok(cache._parseCacheData.called) assert.ok(cache._parseCacheData.firstCall[1], assertFallback) assert.ok(cache._parseCacheData.firstCall[2], assertOptions) }) t.test('should support fallback value if file does not exist', function() { const assertFallback = { a: 1 } let cache = createCache() fsSync.readFileSync.throws(new Error('asdf')) cache._parseCacheData = spy() let output = cache.getSync('bla', assertFallback) assert.strictEqual(output, assertFallback) assert.notOk(cache._parseCacheData.called) }) t.test('parser error should propogate', async function() { const assertError = new Error('Hello') let cache = createCache() cache._parseCacheData = spy().throws(assertError) assert.throws(function() { cache.getSync('asdf') }, assertError) }) }) t.describe('#set()', function() { t.test('should call promise writeFile', async function() { const assertKey = 'asdf1234' const assertInput = { hello: 'world' } const assertContent = JSON.stringify(assertInput) let cache = createCache() cache._parseSetData = spy().returns(assertContent) await cache.set(assertKey, assertInput) assert.ok(fsPromises.writeFile.called) assert.strictEqual(fsPromises.writeFile.firstCall[0], cache.hash(assertKey)) assert.strictEqual(fsPromises.writeFile.firstCall[1], assertContent) assert.strictEqual(fsPromises.writeFile.firstCall[2]?.encoding, 'utf8') assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[0], assertKey) assert.strictEqual(cache._parseSetData.firstCall[1], assertInput) }) t.test('should pass extra options to the parser', async function() { const assertOptions = { a: 1 } let cache = createCache() cache._parseSetData = spy() await cache.set('asdf', null, assertOptions) assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[2], assertOptions) }) t.test('should pass options if number as ttl to the parser', async function() { const assertTtl = 1234 let cache = createCache() cache._parseSetData = spy() await cache.set('asdf', null, assertTtl) assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[2].ttl, assertTtl) }) t.test('should pass extra options to the parser', async function() { const assertEncoding = 'asdf' let cache = createCache() await cache.set('asdf', null, { encoding: assertEncoding }) assert.strictEqual(fsPromises.writeFile.firstCall[2]?.encoding, assertEncoding) }) t.test('parse error should reject properly', async function() { const assertError = new Error('what is up') let cache = createCache() cache._parseSetData = spy().throws(assertError) // make sure it's properly promise wrapped let inbetween = cache.set('asdf') let err = await assert.isRejected(inbetween) assert.strictEqual(err, assertError) }) }) t.describe('#setSync()', function() { t.test('should call sync writeFileSync', function() { const assertKey = 'asdf1234' const assertInput = { hello: 'world' } const assertContent = JSON.stringify(assertInput) let cache = createCache() cache._parseSetData = spy().returns(assertContent) cache.setSync(assertKey, assertInput) assert.ok(fsSync.writeFileSync.called) assert.strictEqual(fsSync.writeFileSync.firstCall[0], cache.hash(assertKey)) assert.strictEqual(fsSync.writeFileSync.firstCall[1], assertContent) assert.strictEqual(fsSync.writeFileSync.firstCall[2]?.encoding, 'utf8') assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[0], assertKey) assert.strictEqual(cache._parseSetData.firstCall[1], assertInput) }) t.test('should pass extra options to the parser', function() { const assertOptions = { a: 1 } let cache = createCache() cache._parseSetData = spy() cache.setSync('asdf', null, assertOptions) assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[2], assertOptions) }) t.test('should pass options if number as ttl to the parser', function() { const assertTtl = 1234 let cache = createCache() cache._parseSetData = spy() cache.setSync('asdf', null, assertTtl) assert.ok(cache._parseSetData.called) assert.strictEqual(cache._parseSetData.firstCall[2].ttl, assertTtl) }) t.test('should pass extra options to the parser', function() { const assertEncoding = 'asdf' let cache = createCache() cache.setSync('asdf', null, { encoding: assertEncoding }) assert.strictEqual(fsSync.writeFileSync.firstCall[2]?.encoding, assertEncoding) }) t.test('parse error should throw', function() { const assertError = new Error('what is up') let cache = createCache() cache._parseSetData = spy().throws(assertError) assert.throws(function() { cache.setSync('asdf') }, assertError) }) }) const setManyFunctions = ['setMany', 'save'] setManyFunctions.forEach(function (fnName) { t.describe(`#${fnName}()`, function () { t.test('should set many at once', async function() { const assertItem1 = { a: 1 } const assertItem2 = { b: 2 } const assertOptions = { opt: true } let cache = createCache() cache.set = spy().resolves() await cache[fnName]([ { key: 'item1', value: assertItem1 }, { key: 'item2', content: assertItem2 }, ], assertOptions) assert.strictEqual(cache.set.callCount, 2) assert.strictEqual(cache.set.getCallN(1)[0], 'item1') assert.strictEqual(cache.set.getCallN(1)[1], assertItem1) assert.strictEqual(cache.set.getCallN(1)[2], assertOptions) assert.strictEqual(cache.set.getCallN(2)[0], 'item2') assert.strictEqual(cache.set.getCallN(2)[1], assertItem2) assert.strictEqual(cache.set.getCallN(2)[2], assertOptions) }) }) }) const setManySyncFunctions = ['setManySync', 'saveSync'] setManySyncFunctions.forEach(function (fnName) { t.describe(`#${fnName}()`, function () { t.test('should setSync many at once', function() { const assertItem1 = { a: 1 } const assertItem2 = { b: 2 } const assertOptions = { opt: true } let cache = createCache() cache.setSync = spy().resolves() cache[fnName]([ { key: 'item1', value: assertItem1 }, { key: 'item2', content: assertItem2 }, ], assertOptions) assert.strictEqual(cache.setSync.callCount, 2) assert.strictEqual(cache.setSync.getCallN(1)[0], 'item1') assert.strictEqual(cache.setSync.getCallN(1)[1], assertItem1) assert.strictEqual(cache.setSync.getCallN(1)[2], assertOptions) assert.strictEqual(cache.setSync.getCallN(2)[0], 'item2') assert.strictEqual(cache.setSync.getCallN(2)[1], assertItem2) assert.strictEqual(cache.setSync.getCallN(2)[2], assertOptions) }) }) }) t.describe('#remove()', function() { t.test('it should call promise rm', async function () { const assertKey = 'asdf1234' let cache = createCache() await cache.remove(assertKey) assert.ok(fsPromises.rm.called) assert.strictEqual(fsPromises.rm.firstCall[0], cache.hash(assertKey)) assert.deepStrictEqual(fsPromises.rm.firstCall[1], { force: true }) }) }) t.describe('#removeSync()', function() { t.test('it should call sync rm', async function () { const assertKey = 'asdf1234' let cache = createCache() await cache.removeSync(assertKey) assert.ok(fsSync.rmSync.called) assert.strictEqual(fsSync.rmSync.firstCall[0], cache.hash(assertKey)) assert.deepStrictEqual(fsSync.rmSync.firstCall[1], { force: true }) }) }) t.describe('#clear()', function() { t.test('it should call promises readdir and rm on every item with same prefix', async function () { let files = ['.gitkeep', '-asdf', '-temp', 'hello.world'] fsPromises.readdir.resolves(files) let cache = createCache() await cache.clear() assert.ok(fsPromises.readdir.called) assert.strictEqual(fsPromises.readdir.firstCall[0], cache.cache_dir) assert.strictEqual(fsPromises.rm.callCount, 2) assert.strictEqual(fsPromises.rm.getCallN(1)[0], path.join(cache.cache_dir, '-asdf')) assert.deepStrictEqual(fsPromises.rm.getCallN(1)[1], { force: true }) assert.strictEqual(fsPromises.rm.getCallN(2)[0], path.join(cache.cache_dir, '-temp')) assert.deepStrictEqual(fsPromises.rm.getCallN(2)[1], { force: true }) }) }) t.describe('#clearSync()', function() { t.test('it should call sync readdirSync and rmSync on every item with same prefix', function () { let files = ['.gitkeep', '-asdf', '-temp', 'hello.world'] fsSync.readdirSync.returns(files) let cache = createCache() cache.clearSync() assert.ok(fsSync.readdirSync.called) assert.strictEqual(fsSync.readdirSync.firstCall[0], cache.cache_dir) assert.strictEqual(fsSync.rmSync.callCount, 2) assert.strictEqual(fsSync.rmSync.getCallN(1)[0], path.join(cache.cache_dir, '-asdf')) assert.deepStrictEqual(fsSync.rmSync.getCallN(1)[1], { force: true }) assert.strictEqual(fsSync.rmSync.getCallN(2)[0], path.join(cache.cache_dir, '-temp')) assert.deepStrictEqual(fsSync.rmSync.getCallN(2)[1], { force: true }) }) }) const getManyFunctions = ['getAll', 'load'] getManyFunctions.forEach(function (fnName) { t.describe(`#${fnName}()`, function () { t.test('should read each file in folder and return results', async function() { let files = ['.gitkeep', '-asdf', '-temp', '-derp', 'hello.world'] let expired = new Date().getTime() - 1 fsPromises.readdir.resolves(files) fsPromises.readFile.onCallN(1).resolves(JSON.stringify({ key: 'a', content: { a: 1 } })) fsPromises.readFile.onCallN(2).resolves(JSON.stringify({ key: 'b', content: { b: 2 } })) fsPromises.readFile.onCallN(3).resolves(JSON.stringify({ key: 'c', content: { c: 3 }, ttl: expired })) let cache = createCache() let data = await cache[fnName]() if (fnName === 'getAll') { assert.strictEqual(data.length, 2) assert.strictEqual(data[0].key, 'a') assert.deepStrictEqual(data[0].content, { a: 1 }) assert.strictEqual(data[1].key, 'b') assert.deepStrictEqual(data[1].content, { b: 2 }) } else { assert.strictEqual(data.files.length, 2) assert.strictEqual(data.files[0].path, cache.hash('a')) assert.strictEqual(data.files[0].key, 'a') assert.deepStrictEqual(data.files[0].value, { a: 1 }) assert.strictEqual(data.files[1].path, cache.hash('b')) assert.strictEqual(data.files[1].key, 'b') assert.deepStrictEqual(data.files[1].value, { b: 2 }) } assert.strictEqual(fsPromises.readFile.getCallN(1)[0], path.join(cache.cache_dir, '-asdf')) assert.strictEqual(fsPromises.readFile.getCallN(2)[0], path.join(cache.cache_dir, '-temp')) assert.strictEqual(fsPromises.readFile.getCallN(3)[0], path.join(cache.cache_dir, '-derp')) }) }) }) })