import fs from 'fs/promises' import { Eltro as t, assert, stub } from 'eltro' import { createContext } from '../helper.server.mjs' import MediaRoutes from '../../api/media/routes.mjs' import { HttpError } from '../../api/error.mjs' t.before(function() { return Promise.all([ fs.readdir('./public/development').then(files => { return Promise.all(files.map(function(file) { if (file !== '.gitkeep') { return fs.unlink(`./public/development/${file}`).catch(function() {}) } return Promise.resolve() })) }), ]) }) t.describe('#filesCacheGet', function() { t.test('should return all files in public folder', async function() { const routes = new MediaRoutes() await routes.init() assert.ok(routes.filesCacheGet('development')) assert.ok(Array.isArray(routes.filesCacheGet('development'))) assert.ok(routes.filesCacheGet('existing')) assert.ok(Array.isArray(routes.filesCacheGet('existing'))) assert.ok(routes.filesCacheGet('nonexisting')) assert.ok(Array.isArray(routes.filesCacheGet('nonexisting'))) assert.strictEqual(routes.filesCacheGet('development').length, 1) assert.strictEqual(routes.filesCacheGet('existing').length, 2) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 0) assert.strictEqual(routes.filesCacheGet('development')[0].filename, '.gitkeep') assert.strictEqual(routes.filesCacheGet('development')[0].size, 0) assert.strictEqual(routes.filesCacheGet('existing')[0].filename, '20220105_101610_test1.jpg') assert.strictEqual(routes.filesCacheGet('existing')[0].size, 15079) assert.strictEqual(routes.filesCacheGet('existing')[1].filename, '20220105_101610_test2.png') assert.strictEqual(routes.filesCacheGet('existing')[1].size, 31705) }) t.test('should always sort result', async function() { const routes = new MediaRoutes() await routes.init() let files = routes.filesCacheGet('existing') assert.strictEqual(files[0].filename, '20220105_101610_test1.jpg') assert.strictEqual(files[0].size, 15079) assert.strictEqual(files[1].filename, '20220105_101610_test2.png') assert.strictEqual(files[1].size, 31705) routes.siteCache.get('existing').push({ filename: '0000.png', size: 0 }) files = routes.filesCacheGet('existing') assert.strictEqual(files[0].filename, '0000.png') assert.strictEqual(files[0].size, 0) assert.strictEqual(files[1].filename, '20220105_101610_test1.jpg') assert.strictEqual(files[1].size, 15079) assert.strictEqual(files[2].filename, '20220105_101610_test2.png') assert.strictEqual(files[2].size, 31705) }) }) t.describe('#listFiles()', function() { const stubVerify = stub() const stubGetCache = stub() const routes = new MediaRoutes({ security: { verifyToken: stubVerify }, }) routes.filesCacheGet = stubGetCache function reset() { stubVerify.reset() stubGetCache.reset() } t.test('should call security correctly', async function() { reset() let ctx = createContext() const assertError = new Error('temp') stubVerify.rejects(assertError) let err = await assert.isRejected(routes.listFiles(ctx)) assert.ok(stubVerify.called) assert.strictEqual(err, assertError) assert.strictEqual(stubVerify.firstCall[0], ctx) }) t.test('should call filesCacheGet and return results', async function() { reset() let ctx = createContext() const assertSiteName = 'benshapiro' const assertResult = { a: 1 } stubVerify.resolves(assertSiteName) stubGetCache.returns(assertResult) await routes.listFiles(ctx) assert.ok(stubGetCache.called) assert.strictEqual(stubGetCache.firstCall[0], assertSiteName) assert.strictEqual(ctx.body, assertResult) }) }) t.describe('#listPublicFiles()', function() { const stubSitePublic = stub() const stubGetCache = stub() const routes = new MediaRoutes({ security: { throwIfNotPublic: stubSitePublic }, }) routes.filesCacheGet = stubGetCache function reset() { stubSitePublic.reset() stubGetCache.reset() } t.test('should call security correctly', async function() { reset() let ctx = createContext() const assertError = new Error('temp') const assertSite = 'astalavista' stubSitePublic.throws(assertError) ctx.params.site = assertSite let err = await assert.isRejected(routes.listPublicFiles(ctx)) assert.ok(stubSitePublic.called) assert.strictEqual(err, assertError) assert.strictEqual(stubSitePublic.firstCall[0], assertSite) }) t.test('should call filesCacheGet and return results', async function() { reset() let ctx = createContext() const assertSiteName = 'benshapiro' const assertResult = { a: 1 } ctx.params.site = assertSiteName stubGetCache.returns(assertResult) await routes.listPublicFiles(ctx) assert.ok(stubGetCache.called) assert.strictEqual(stubGetCache.firstCall[0], assertSiteName) assert.strictEqual(ctx.body, assertResult) }) }) t.describe('#filesCacheAdd', function() { t.test('should auto-create array of site and add file', async function() { const routes = new MediaRoutes() const assertName = 'asdf.png' const assertSize = 1234 await routes.init() assert.ok(routes.filesCacheGet('nonexisting')) assert.ok(Array.isArray(routes.filesCacheGet('nonexisting'))) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 0) routes.filesCacheAdd('nonexisting', assertName, assertSize) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 1) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].filename, assertName) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].size, assertSize) }) t.test('should not add but update size if file already exists', function() { const routes = new MediaRoutes() const assertName = 'asdf.png' const assertSize = 1234 const assertNewSize = 5678 assert.ok(routes.filesCacheGet('nonexisting')) assert.ok(Array.isArray(routes.filesCacheGet('nonexisting'))) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 0) routes.filesCacheAdd('nonexisting', assertName, assertSize) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 1) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].filename, assertName) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].size, assertSize) routes.filesCacheAdd('nonexisting', assertName, assertNewSize) assert.strictEqual(routes.filesCacheGet('nonexisting').length, 1) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].filename, assertName) assert.strictEqual(routes.filesCacheGet('nonexisting')[0].size, assertNewSize) }) }) t.describe('#filesCacheRemove', function() { t.test('should default not do anything if nothing is found', async function() { const routes = new MediaRoutes() await routes.init() assert.ok(routes.filesCacheGet('existing')) assert.ok(Array.isArray(routes.filesCacheGet('existing'))) assert.strictEqual(routes.filesCacheGet('existing').length, 2) routes.filesCacheRemove('nonexisting', 'bla.text') routes.filesCacheRemove('nonexisting', 'herp.derp') routes.filesCacheRemove('existing', 'bla.text') routes.filesCacheRemove('existing', 'herp.derp') assert.ok(routes.filesCacheGet('existing')) assert.ok(Array.isArray(routes.filesCacheGet('existing'))) assert.strictEqual(routes.filesCacheGet('existing').length, 2) }) t.test('should otherwise remove entries that match filename', async function() { const routes = new MediaRoutes() await routes.init() assert.ok(routes.filesCacheGet('existing')) assert.ok(Array.isArray(routes.filesCacheGet('existing'))) assert.strictEqual(routes.filesCacheGet('existing').length, 2) routes.filesCacheRemove('existing', '20220105_101610_test1.jpg') assert.ok(routes.filesCacheGet('existing')) assert.ok(Array.isArray(routes.filesCacheGet('existing'))) assert.strictEqual(routes.filesCacheGet('existing').length, 1) assert.strictEqual(routes.filesCacheGet('existing')[0].filename, '20220105_101610_test2.png') }) }) t.describe('#uploadNoPrefix', function() { const stubVerify = stub() const stubUpload = stub() const stubStat = stub() const routes = new MediaRoutes({ security: { verifyToken: stubVerify, verifyBody: stub(), }, formidable: { uploadFile: stubUpload, }, fs: { stat: stubStat }, }) function reset() { stubVerify.reset() stubUpload.reset() stubStat.reset() } t.test('should call upload correctly', async function() { reset() let ctx = createContext() const assertSiteName = 'benshapiro' const assertError = new Error('hello') stubVerify.resolves(assertSiteName) stubUpload.rejects(assertError) let err = await assert.isRejected(routes.uploadNoPrefix(ctx)) assert.ok(stubUpload.called) assert.strictEqual(err, assertError) assert.strictEqual(stubUpload.firstCall[0], ctx) assert.strictEqual(stubUpload.firstCall[1], assertSiteName) assert.strictEqual(stubUpload.firstCall[2], true) }) }) t.describe('#resize', function() { const stubVerifyToken = stub() const stubVerifyBody = stub() const stubUpload = stub() const stubSharp = stub() const stubSharpResize = stub() const stubSharpBlur = stub() const stubSharpTrim = stub() const stubSharpExtend = stub() const stubSharpFlatten = stub() const stubSharpRotate = stub() const stubSharpToFile = stub() const stubSharpJpeg = stub() const stubSharpPng = stub() const stubSharpToBuffer = stub() const stubStat = stub() const routes = new MediaRoutes({ security: { verifyToken: stubVerifyToken, verifyBody: stubVerifyBody, }, fs: { stat: stubStat }, formidable: { uploadFile: stubUpload }, sharp: stubSharp, }) function reset() { stubVerifyToken.reset() stubVerifyBody.reset() stubUpload.reset() let def = { toFile: stubSharpToFile, jpeg: stubSharpJpeg, png: stubSharpPng, resize: stubSharpResize, trim: stubSharpTrim, extend: stubSharpExtend, flatten: stubSharpFlatten, blur: stubSharpBlur, rotate: stubSharpRotate, toBuffer: stubSharpToBuffer, } stubStat.reset() stubStat.resolves({ size: 0 }) stubSharp.reset() stubSharp.returns(def) for (let key in def) { def[key].reset() def[key].returns(def) } stubSharpToFile.resolves() stubSharpToBuffer.resolves() routes.siteCache.clear() } t.test('should call security verifyBody correctly', async function() { reset() let ctx = createContext({ req: { body: { }}}) const assertError = new Error('temp') stubUpload.resolves({ filename: 'bla' }) stubVerifyBody.throws(assertError) let err = await assert.isRejected(routes.resize(ctx)) assert.strictEqual(err, assertError) assert.ok(stubVerifyBody.called) assert.strictEqual(stubVerifyBody.firstCall[0], ctx) }) t.test('should call sharp correctly if items are specified', async function() { reset() const assertKey = 'asdf' const assertJpeg = { a: 1 } const assertFilename = 'asdfsafd' const assertSite = 'mario' stubVerifyToken.resolves(assertSite) stubUpload.resolves({ filename: assertFilename + '.png' }) let ctx = createContext({ req: { body: { [assertKey]: { format: 'jpeg', } }}}) ctx.req.body[assertKey].jpeg = assertJpeg await routes.resize(ctx) assert.ok(stubSharp.called) assert.match(stubSharp.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}\.png`)) assert.ok(stubSharpRotate.called) assert.ok(stubSharpJpeg.called) assert.strictEqual(stubSharpJpeg.firstCall[0], assertJpeg) assert.ok(stubSharpToFile.called) assert.match(stubSharpToFile.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}_${assertKey}\.jpg`)) assert.notOk(stubSharpPng.called) assert.notOk(stubSharpResize.called) assert.strictEqual(ctx.body.filename, assertFilename + '.png') assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}.png`) assert.strictEqual(ctx.body[assertKey].filename, `${assertFilename}_${assertKey}\.jpg`) assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.jpg`) }) let validOperations = [ 'resize', 'blur', 'trim', 'extend', 'flatten', ] let operationStubMap = { resize: stubSharpResize, blur: stubSharpBlur, trim: stubSharpTrim, extend: stubSharpExtend, flatten: stubSharpFlatten, } validOperations.forEach(function(operation) { t.test(`should call sharp correctly if items and ${operation} are specified`, async function() { reset() const assertKey = 'herpderp' const assertPng = { a: 1 } const assertPayload = { a: 1 } const assertFilename = 'asdfsafd' const assertSite = 'mario' stubVerifyToken.resolves(assertSite) stubUpload.resolves({ filename: assertFilename + '.png' }) let called = 0 stubStat.returnWith(function() { called += 10 return { size: called } }) let ctx = createContext({ req: { body: { [assertKey]: { format: 'png', } }}}) ctx.req.body[assertKey].png = assertPng ctx.req.body[assertKey][operation] = assertPayload await routes.resize(ctx) assert.ok(stubSharp.called) assert.match(stubSharp.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}\.png`)) assert.ok(stubSharpRotate.called) assert.ok(stubSharpPng.called) assert.strictEqual(stubSharpPng.firstCall[0], assertPng) assert.ok(stubSharpToFile.called) assert.match(stubSharpToFile.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}_${assertKey}\.png`)) assert.notOk(stubSharpJpeg.called) assert.ok(operationStubMap[operation].called) assert.strictEqual(operationStubMap[operation].firstCall[0], assertPayload) assert.strictEqual(ctx.body.filename, assertFilename + '.png') assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}.png`) assert.strictEqual(ctx.body[assertKey].filename, `${assertFilename}_${assertKey}\.png`) assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.png`) let filesFromCache = routes.filesCacheGet(assertSite) assert.strictEqual(filesFromCache.length, 2) assert.strictEqual(filesFromCache[0].filename, `${assertFilename}_${assertKey}\.png`) assert.strictEqual(filesFromCache[0].size, 20) assert.strictEqual(filesFromCache[1].filename, assertFilename + '.png') assert.strictEqual(filesFromCache[1].size, 10) }) }) t.test('should notify which item failed if one fails', async function() { reset() const assertValidKey1 = 'herp' const assertValidKey2 = 'derp' const assertErrorKey = 'throwmyerr' const assertErrorMessage = 'some message here' stubVerifyToken.resolves('asdf') stubUpload.resolves({ filename: 'file.png' }) let called = 0 stubStat.returnWith(function() { called += 10 return { size: called } }) let ctx = createContext({ req: { body: { [assertValidKey1]: { format: 'png', png: { a: 1 }, }, [assertValidKey2]: { format: 'png', png: { a: 1 }, }, [assertErrorKey]: { format: 'png', png: { a: 1 }, }, }}}) stubSharpToFile.returnWith(function(file) { if (file.match(new RegExp(assertErrorKey))) { throw new Error(assertErrorMessage) } }) let err = await assert.isRejected(routes.resize(ctx)) assert.ok(err instanceof HttpError) assert.ok(err instanceof Error) assert.strictEqual(err.status, 422) assert.match(err.message, new RegExp(assertErrorKey)) assert.match(err.message, new RegExp(assertErrorMessage)) }) t.test('should call sharp correctly and return base64 format if out is base64', async function() { reset() const assertKey = 'outtest' const assertFilename = 'asdfsafd.png' const assertSite = 'mario' const assertBase64Data = 'asdf1234' stubVerifyToken.resolves(assertSite) stubUpload.resolves({ filename: assertFilename }) let ctx = createContext({ req: { body: { [assertKey]: { format: 'png', png: { a: 1 }, out: 'base64', } }}}) stubSharpToBuffer.resolves(Buffer.from(assertBase64Data)) await routes.resize(ctx) assert.ok(stubSharp.called) assert.notOk(stubSharpToFile.called) assert.ok(stubSharpToBuffer.called) assert.ok(ctx.body[assertKey].base64) assert.ok(ctx.body[assertKey].base64.startsWith(`data:image/png;base64,`)) let base64 = ctx.body[assertKey].base64 let bufferBase64 = Buffer.from(base64.slice(base64.indexOf(',')), 'base64') assert.strictEqual(bufferBase64.toString(), assertBase64Data) let filesFromCache = routes.filesCacheGet(assertSite) assert.strictEqual(filesFromCache.length, 1) assert.strictEqual(filesFromCache[0].filename, assertFilename) }) }) let basicUploadTestRoutes = [ 'upload', 'resize', 'uploadNoPrefix' ] basicUploadTestRoutes.forEach(function(name) { t.describe(`#${name}() Base`, function() { const stubVerify = stub() const stubUpload = stub() const stubStat = stub() const routes = new MediaRoutes({ security: { verifyToken: stubVerify, verifyBody: stub(), }, formidable: { uploadFile: stubUpload, }, fs: { stat: stubStat }, }) function reset() { stubVerify.reset() stubUpload.reset() stubStat.reset() } t.test('should call security correctly', async function() { reset() let ctx = createContext({ req: { body: { } } }) const assertError = new Error('temp') stubVerify.rejects(assertError) let err = await assert.isRejected(routes[name](ctx)) assert.ok(stubVerify.called) assert.strictEqual(err, assertError) assert.strictEqual(stubVerify.firstCall[0], ctx) }) t.test('should call upload correctly', async function() { reset() let ctx = createContext({ req: { body: { } } }) const assertSiteName = 'benshapiro' const assertError = new Error('hello') stubVerify.resolves(assertSiteName) stubUpload.rejects(assertError) let err = await assert.isRejected(routes[name](ctx)) assert.ok(stubUpload.called) assert.strictEqual(err, assertError) assert.strictEqual(stubUpload.firstCall[0], ctx) assert.strictEqual(stubUpload.firstCall[1], assertSiteName) }) t.test('should otherwise return the file in result', async function() { reset() let ctx = createContext({ req: { body: { } } }) const assertSize = 1241412 const assertFilename = 'asdfsafd' const assertSite = 'mario' stubVerify.resolves(assertSite) stubUpload.resolves({ filename: assertFilename }) stubStat.resolves({ size: assertSize }) await routes[name](ctx) assert.strictEqual(ctx.body.filename, assertFilename) assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}`) assert.ok(stubStat.called) assert.strictEqual(stubStat.firstCall[0], `./public/${assertSite}/${assertFilename}`) let filesFromCache = routes.filesCacheGet(assertSite) assert.strictEqual(filesFromCache.length, 1) assert.strictEqual(filesFromCache[0].filename, assertFilename) assert.strictEqual(filesFromCache[0].size, assertSize) }) }) }) t.describe('#remove()', function() { const stubVerify = stub() const stubUnlink = stub() const stubCacheRemove = stub() const routes = new MediaRoutes({ security: { verifyToken: stubVerify, }, fs: { unlink: stubUnlink }, }) routes.filesCacheRemove = stubCacheRemove function reset() { stubVerify.reset() stubUnlink.reset() stubCacheRemove.reset() stubUnlink.resolves(null) } t.test('should call security correctly', async function() { reset() let ctx = createContext({ req: { body: { } } }) const assertError = new Error('temp') stubVerify.rejects(assertError) let err = await assert.isRejected(routes.remove(ctx)) assert.ok(stubVerify.called) assert.strictEqual(err, assertError) assert.strictEqual(stubVerify.firstCall[0], ctx) }) t.test('should call unlink correctly', async function() { const assertSiteName = 'benshapiro' const assertFilename = 'somefilename.png' const assertErrorMessage = 'Noriyasu Agematsu Tensei' const assertError = new Error(assertErrorMessage) reset() let ctx = createContext({ req: { body: { } } }) ctx.params.filename = assertFilename stubVerify.resolves(assertSiteName) stubUnlink.rejects(assertError) let err = await assert.isRejected(routes.remove(ctx)) assert.ok(stubUnlink.called) assert.ok(err instanceof HttpError) assert.ok(err instanceof Error) assert.strictEqual(err.status, 422) assert.match(err.message, new RegExp(assertSiteName)) assert.match(err.message, new RegExp(assertFilename)) assert.match(err.message, new RegExp(assertErrorMessage)) assert.strictEqual(stubUnlink.firstCall[0], `./public/${assertSiteName}/${assertFilename}`) }) t.test('should otherwise return 204 status and remove from array', async function() { const assertSiteName = 'benshapiro' const assertFilename = 'somefilename.png' reset() let ctx = createContext({ req: { body: { } } }) ctx.params.filename = assertFilename stubVerify.resolves(assertSiteName) await routes.remove(ctx) assert.strictEqual(ctx.status, 204) assert.ok(stubCacheRemove.called) assert.strictEqual(stubCacheRemove.firstCall[0], assertSiteName) assert.strictEqual(stubCacheRemove.firstCall[1], assertFilename) }) })