From a451e562fbf4ad234f02158daec8ad69da0ae93e Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Tue, 6 Dec 2022 00:01:38 -0600 Subject: [PATCH] add initial set of tests for api --- server/homebrew.api.js | 606 ++++++++++++++++++------------------ server/homebrew.api.spec.js | 377 ++++++++++++++++++++++ 2 files changed, 674 insertions(+), 309 deletions(-) create mode 100644 server/homebrew.api.spec.js diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 563926f01..2203367ef 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -15,328 +15,316 @@ const { nanoid } = require('nanoid'); // }); // }; -const getId = (req)=>{ - // Set the id and initial potential google id, where the google id is present on the existing brew. - let id = req.params.id, googleId = req.body?.googleId; - - // If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up. - if(id.length > 12) { - googleId = id.slice(0, -12); - id = id.slice(-12); - } - return { id, googleId }; -}; - -const getBrew = (accessType, fetchGoogle = true)=>{ - // Create middleware with the accessType passed in as part of the scope - return async (req, res, next)=>{ - // Get relevant IDs for the brew - const { id, googleId } = getId(req); - - // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. - let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id }) - .catch((err)=>{ - if(googleId) { - console.warn(`Unable to find document stub for ${accessType}Id ${id}`); - } else { - console.warn(err); - } - }); - stub = stub?.toObject(); - if(stub?.authors && !stub?.authors.includes(req.account.username)) { - throw 'Current logged in user does not have access to this brew.'; - } - - // If there is a google id, try to find the google brew - if(fetchGoogle && (googleId || stub?.googleId)) { - let googleError; - const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType) - .catch((err)=>{ - console.warn(err); - googleError = err; - }); - // If we can't find the google brew and there is a google id for the brew, throw an error. - if(!googleBrew) throw googleError; - // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew - stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew; - } - - // If after all of that we still don't have a brew, throw an exception - if(!stub && fetchGoogle) { - throw 'Brew not found in Homebrewery database or Google Drive'; - } - - if(typeof stub?.tags === 'string') { - stub.tags = []; - } - req.brew = stub || {}; - - next(); - }; -}; - -const mergeBrewText = (brew)=>{ - let text = brew.text; - if(brew.style !== undefined) { - text = `\`\`\`css\n` + - `${brew.style || ''}\n` + - `\`\`\`\n\n` + - `${text}`; - } - const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']); - text = `\`\`\`metadata\n` + - `${yaml.dump(metadata)}\n` + - `\`\`\`\n\n` + - `${text}`; - return text; -}; - const MAX_TITLE_LENGTH = 100; -const getGoodBrewTitle = (text)=>{ - const tokens = Markdown.marked.lexer(text); - return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title') - .slice(0, MAX_TITLE_LENGTH); -}; +const api = { + homebrewApi : router, + getId : (req)=>{ + // Set the id and initial potential google id, where the google id is present on the existing brew. + let id = req.params.id, googleId = req.body?.googleId; -const excludePropsFromUpdate = (brew)=>{ - // Remove undesired properties - const modified = _.clone(brew); - const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId']; - for (const prop of propsToExclude) { - delete modified[prop]; - } - return modified; -}; - -const excludeGoogleProps = (brew)=>{ - const modified = _.clone(brew); - const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail']; - for (const prop of propsToExclude) { - delete modified[prop]; - } - return modified; -}; - -const excludeStubProps = (brew)=>{ - const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version']; - for (const prop of propsToExclude) { - brew[prop] = undefined; - } - return brew; -}; - -const beforeNewSave = (account, brew)=>{ - if(!brew.title) { - brew.title = getGoodBrewTitle(brew.text); - } - - brew.authors = (account) ? [account.username] : []; - brew.text = mergeBrewText(brew); -}; - -const newGoogleBrew = async (account, brew, res)=>{ - const oAuth2Client = GoogleActions.authCheck(account, res); - - const newBrew = excludeGoogleProps(brew); - - return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew); -}; - -const newBrew = async (req, res)=>{ - const brew = req.body; - const { saveToGoogle } = req.query; - - delete brew.editId; - delete brew.shareId; - delete brew.googleId; - - beforeNewSave(req.account, brew); - - const newHomebrew = new HomebrewModel(brew); - newHomebrew.editId = nanoid(12); - newHomebrew.shareId = nanoid(12); - - let googleId, saved; - if(saveToGoogle) { - googleId = await newGoogleBrew(req.account, newHomebrew, res) - .catch((err)=>{ - console.error(err); - res.status(err?.status || err?.response?.status || 500).send(err?.message || err); - }); - if(!googleId) return; - excludeStubProps(newHomebrew); - newHomebrew.googleId = googleId; - } else { - // Compress brew text to binary before saving - newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text); - // Delete the non-binary text field since it's not needed anymore - newHomebrew.text = undefined; - } - - saved = await newHomebrew.save() - .catch((err)=>{ - console.error(err, err.toString(), err.stack); - throw `Error while creating new brew, ${err.toString()}`; - }); - if(!saved) return; - saved = saved.toObject(); - - res.status(200).send(saved); -}; - -const updateBrew = async (req, res)=>{ - // Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method - let brew = _.assign(req.brew, excludePropsFromUpdate(req.body)); - const { saveToGoogle, removeFromGoogle } = req.query; - const googleId = brew.googleId; - let afterSave = async ()=>true; - - brew.text = mergeBrewText(brew); - - if(brew.googleId && removeFromGoogle) { - // If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined - afterSave = async ()=>{ - return await deleteGoogleBrew(req.account, googleId, brew.editId, res) - .catch((err)=>{ - console.error(err); - res.status(err?.status || err?.response?.status || 500).send(err.message || err); - }); - }; - - brew.googleId = undefined; - } else if(!brew.googleId && saveToGoogle) { - // If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew - brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res) - .catch((err)=>{ - console.error(err); - res.status(err.status || err.response.status).send(err.message || err); - }); - if(!brew.googleId) return; - } else if(brew.googleId) { - // If the google id exists and no other actions are being performed, update the google brew - const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew)) - .catch((err)=>{ - console.error(err); - res.status(err?.response?.status || 500).send(err); - }); - if(!updated) return; - } - - if(brew.googleId) { - // If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items - excludeStubProps(brew); - } else { - // Compress brew text to binary before saving - brew.textBin = zlib.deflateRawSync(brew.text); - // Delete the non-binary text field since it's not needed anymore - brew.text = undefined; - } - brew.updatedAt = new Date(); - - if(req.account) { - brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); - } - // we need the tag type change in both getBrew and here to handle the case where we don't have a stub on which to perform the modification - if(typeof brew.tags === 'string') { - brew.tags = []; - } - - // define a function to catch our save errors - const saveError = (err)=>{ - console.error(err); - res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database'); - }; - let saved; - if(!brew._id) { - // if the brew does not have a stub id, create and save it, then write the new value back to the brew. - saved = await new HomebrewModel(brew).save().catch(saveError); - brew = saved?.toObject(); - } else { - // if the brew does have a stub id, update it using the stub id as the key. - saved = await HomebrewModel.updateOne({ _id: brew._id }, brew).catch(saveError); - } - if(!saved) return; - // Call and wait for afterSave to complete - const after = await afterSave(); - if(!after) return; - - res.status(200).send(brew); -}; - -const deleteGoogleBrew = async (account, id, editId, res)=>{ - const auth = await GoogleActions.authCheck(account, res); - await GoogleActions.deleteGoogleBrew(auth, id, editId); - return true; -}; - -const deleteBrew = async (req, res, next)=>{ - // Delete an orphaned stub if its Google brew doesn't exist - try { - await getBrew('edit')(req, res, ()=>{}); - } catch (err) { - const { id, googleId } = getId(req); - console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`); - await HomebrewModel.deleteOne({ editId: id }); - return next(); - } - - let brew = req.brew; - const { googleId, editId } = brew; - const account = req.account; - const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username); - // If the user is the owner and the file is saved to google, mark the google brew for deletion - const shouldDeleteGoogleBrew = googleId && isOwner; - - if(brew._id) { - brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); - if(account) { - // Remove current user as author - brew.authors = _.pull(brew.authors, account.username); - brew.markModified('authors'); + // If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up. + if(id.length > 12) { + googleId = id.slice(0, -12); + id = id.slice(-12); } + return { id, googleId }; + }, + getBrew : (accessType, fetchGoogle = true)=>{ + // Create middleware with the accessType passed in as part of the scope + return async (req, res, next)=>{ + // Get relevant IDs for the brew + const { id, googleId } = api.getId(req); - if(brew.authors.length === 0) { - // Delete brew if there are no authors left - await brew.remove() + // Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine. + let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id }) .catch((err)=>{ - console.error(err); - throw { status: 500, message: 'Error while removing' }; + if(googleId) { + console.warn(`Unable to find document stub for ${accessType}Id ${id}`); + } else { + console.warn(err); + } }); - } else { - if(shouldDeleteGoogleBrew) { - // When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database - brew.googleId = undefined; - brew.textBin = zlib.deflateRawSync(brew.text); - brew.text = undefined; + stub = stub?.toObject(); + if(accessType === 'edit' && stub?.authors?.length > 0 && !stub?.authors.includes(req.account?.username)) { + throw 'Current logged in user does not have access to this brew.'; } - // Otherwise, save the brew with updated author list - await brew.save() - .catch((err)=>{ - throw { status: 500, message: err }; - }); + // If there is a google id, try to find the google brew + if(fetchGoogle && (googleId || stub?.googleId)) { + let googleError; + const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType) + .catch((err)=>{ + console.warn(err); + googleError = err; + }); + // If we can't find the google brew and there is a google id for the brew, throw an error. + if(!googleBrew) throw googleError; + // Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew + stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew; + } + + // If after all of that we still don't have a brew, throw an exception + if(!stub && fetchGoogle) { + throw 'Brew not found in Homebrewery database or Google Drive'; + } + + if(typeof stub?.tags === 'string') { + stub.tags = []; + } + req.brew = stub || {}; + + next(); + }; + }, + mergeBrewText : (brew)=>{ + let text = brew.text; + if(brew.style !== undefined) { + text = `\`\`\`css\n` + + `${brew.style || ''}\n` + + `\`\`\`\n\n` + + `${text}`; } - } - if(shouldDeleteGoogleBrew) { - const deleted = await deleteGoogleBrew(account, googleId, editId, res) + const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']); + text = `\`\`\`metadata\n` + + `${yaml.dump(metadata)}\n` + + `\`\`\`\n\n` + + `${text}`; + return text; + }, + getGoodBrewTitle : (text)=>{ + const tokens = Markdown.marked.lexer(text); + return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title') + .slice(0, MAX_TITLE_LENGTH); + }, + excludePropsFromUpdate : (brew)=>{ + // Remove undesired properties + const modified = _.clone(brew); + const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId']; + for (const prop of propsToExclude) { + delete modified[prop]; + } + return modified; + }, + excludeGoogleProps : (brew)=>{ + const modified = _.clone(brew); + const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail']; + for (const prop of propsToExclude) { + delete modified[prop]; + } + return modified; + }, + excludeStubProps : (brew)=>{ + const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version']; + for (const prop of propsToExclude) { + brew[prop] = undefined; + } + return brew; + }, + beforeNewSave : (account, brew)=>{ + if(!brew.title) { + brew.title = api.getGoodBrewTitle(brew.text); + } + + brew.authors = (account) ? [account.username] : []; + brew.text = api.mergeBrewText(brew); + }, + newGoogleBrew : async (account, brew, res)=>{ + const oAuth2Client = GoogleActions.authCheck(account, res); + + const newBrew = api.excludeGoogleProps(brew); + + return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew); + }, + newBrew : async (req, res)=>{ + const brew = req.body; + const { saveToGoogle } = req.query; + + delete brew.editId; + delete brew.shareId; + delete brew.googleId; + + api.beforeNewSave(req.account, brew); + + const newHomebrew = new HomebrewModel(brew); + newHomebrew.editId = nanoid(12); + newHomebrew.shareId = nanoid(12); + + let googleId, saved; + if(saveToGoogle) { + googleId = await api.newGoogleBrew(req.account, newHomebrew, res) + .catch((err)=>{ + console.error(err); + res.status(err?.status || err?.response?.status || 500).send(err?.message || err); + }); + if(!googleId) return; + api.excludeStubProps(newHomebrew); + newHomebrew.googleId = googleId; + } else { + // Compress brew text to binary before saving + newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text); + // Delete the non-binary text field since it's not needed anymore + newHomebrew.text = undefined; + } + + saved = await newHomebrew.save() .catch((err)=>{ - console.error(err); - res.status(500).send(err); + console.error(err, err.toString(), err.stack); + throw `Error while creating new brew, ${err.toString()}`; }); - if(!deleted) return; + if(!saved) return; + saved = saved.toObject(); + + res.status(200).send(saved); + }, + updateBrew : async (req, res)=>{ + // Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method + let brew = _.assign(req.brew, api.excludePropsFromUpdate(req.body)); + const { saveToGoogle, removeFromGoogle } = req.query; + const googleId = brew.googleId; + let afterSave = async ()=>true; + + brew.text = api.mergeBrewText(brew); + + if(brew.googleId && removeFromGoogle) { + // If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined + afterSave = async ()=>{ + return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res) + .catch((err)=>{ + console.error(err); + res.status(err?.status || err?.response?.status || 500).send(err.message || err); + }); + }; + + brew.googleId = undefined; + } else if(!brew.googleId && saveToGoogle) { + // If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew + brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res) + .catch((err)=>{ + console.error(err); + res.status(err.status || err.response.status).send(err.message || err); + }); + if(!brew.googleId) return; + } else if(brew.googleId) { + // If the google id exists and no other actions are being performed, update the google brew + const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew)) + .catch((err)=>{ + console.error(err); + res.status(err?.response?.status || 500).send(err); + }); + if(!updated) return; + } + + if(brew.googleId) { + // If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items + api.excludeStubProps(brew); + } else { + // Compress brew text to binary before saving + brew.textBin = zlib.deflateRawSync(brew.text); + // Delete the non-binary text field since it's not needed anymore + brew.text = undefined; + } + brew.updatedAt = new Date(); + + if(req.account) { + brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); + } + // we need the tag type change in both getBrew and here to handle the case where we don't have a stub on which to perform the modification + if(typeof brew.tags === 'string') { + brew.tags = []; + } + + // define a function to catch our save errors + const saveError = (err)=>{ + console.error(err); + res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database'); + }; + let saved; + if(!brew._id) { + // if the brew does not have a stub id, create and save it, then write the new value back to the brew. + saved = await new HomebrewModel(brew).save().catch(saveError); + brew = saved?.toObject(); + } else { + // if the brew does have a stub id, update it using the stub id as the key. + saved = await HomebrewModel.updateOne({ _id: brew._id }, brew).catch(saveError); + } + if(!saved) return; + // Call and wait for afterSave to complete + const after = await afterSave(); + if(!after) return; + + res.status(200).send(brew); + }, + deleteGoogleBrew : async (account, id, editId, res)=>{ + const auth = await GoogleActions.authCheck(account, res); + await GoogleActions.deleteGoogleBrew(auth, id, editId); + return true; + }, + deleteBrew : async (req, res, next)=>{ + // Delete an orphaned stub if its Google brew doesn't exist + try { + await api.getBrew('edit')(req, res, ()=>{}); + } catch (err) { + const { id, googleId } = api.getId(req); + console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`); + await HomebrewModel.deleteOne({ editId: id }); + return next(); + } + + let brew = req.brew; + const { googleId, editId } = brew; + const account = req.account; + const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username); + // If the user is the owner and the file is saved to google, mark the google brew for deletion + const shouldDeleteGoogleBrew = googleId && isOwner; + + if(brew._id) { + brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew); + if(account) { + // Remove current user as author + brew.authors = _.pull(brew.authors, account.username); + brew.markModified('authors'); + } + + if(brew.authors.length === 0) { + // Delete brew if there are no authors left + await brew.remove() + .catch((err)=>{ + console.error(err); + throw { status: 500, message: 'Error while removing' }; + }); + } else { + if(shouldDeleteGoogleBrew) { + // When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database + brew.googleId = undefined; + brew.textBin = zlib.deflateRawSync(brew.text); + brew.text = undefined; + } + + // Otherwise, save the brew with updated author list + await brew.save() + .catch((err)=>{ + throw { status: 500, message: err }; + }); + } + } + if(shouldDeleteGoogleBrew) { + const deleted = await api.deleteGoogleBrew(account, googleId, editId, res) + .catch((err)=>{ + console.error(err); + res.status(500).send(err); + }); + if(!deleted) return; + } + + res.status(204).send(); } - - res.status(204).send(); }; -router.post('/api', asyncHandler(newBrew)); -router.put('/api/:id', asyncHandler(getBrew('edit', false)), asyncHandler(updateBrew)); -router.put('/api/update/:id', asyncHandler(getBrew('edit', false)), asyncHandler(updateBrew)); -router.delete('/api/:id', asyncHandler(deleteBrew)); -router.get('/api/remove/:id', asyncHandler(deleteBrew)); +router.post('/api', asyncHandler(api.newBrew)); +router.put('/api/:id', asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.put('/api/update/:id', asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew)); +router.delete('/api/:id', asyncHandler(api.deleteBrew)); +router.get('/api/remove/:id', asyncHandler(api.deleteBrew)); -module.exports = { - homebrewApi : router, - getBrew -}; +module.exports = api; diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js new file mode 100644 index 000000000..fbf5cff0b --- /dev/null +++ b/server/homebrew.api.spec.js @@ -0,0 +1,377 @@ +/* eslint-disable max-lines */ + +describe('Tests for api', ()=>{ + let api; + let google; + let model; + let hbBrew; + let googleBrew; + + beforeEach(()=>{ + google = require('./googleActions.js'); + model = require('./homebrew.model.js').model; + + jest.mock('./googleActions.js'); + jest.mock('./homebrew.model.js'); + + api = require('./homebrew.api'); + + hbBrew = { + text : `brew text`, + style : 'hello yes i am css', + title : 'some title', + description : 'this is a description', + tags : ['something', 'fun'], + systems : ['D&D 5e'], + renderer : 'v3', + theme : 'phb', + published : true, + authors : ['1', '2'], + owner : '1', + thumbnail : '', + _id : 'mongoid', + editId : 'abcdefg', + shareId : 'hijklmnop', + views : 1, + lastViewed : new Date(), + version : 1, + pageCount : 1, + textBin : '' + }; + googleBrew = { + ...hbBrew, + googleId : '12345' + }; + }); + + afterEach(()=>{ + jest.restoreAllMocks(); + }); + + describe('getId', ()=>{ + it('should return only id if google id is not present', ()=>{ + const { id, googleId } = api.getId({ + params : { + id : 'abcdefgh' + } + }); + + expect(id).toEqual('abcdefgh'); + expect(googleId).toBeUndefined(); + }); + + it('should return id and google id from request body', ()=>{ + const { id, googleId } = api.getId({ + params : { + id : 'abcdefgh' + }, + body : { + googleId : '12345' + } + }); + + expect(id).toEqual('abcdefgh'); + expect(googleId).toEqual('12345'); + }); + + it('should return id and google id params', ()=>{ + const { id, googleId } = api.getId({ + params : { + id : '123456789012abcdefghijkl' + } + }); + + expect(id).toEqual('abcdefghijkl'); + expect(googleId).toEqual('123456789012'); + }); + }); + + describe('getBrew', ()=>{ + const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); + const notFoundError = 'Brew not found in Homebrewery database or Google Drive'; + + it('returns middleware', ()=>{ + const getFn = api.getBrew('share'); + expect(getFn).toBeInstanceOf(Function); + }); + + it('should fetch from mongoose', async ()=>{ + const testBrew = { title: 'test brew', authors: [] }; + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>toBrewPromise(testBrew)); + + const fn = api.getBrew('share', true); + const req = { brew: {} }; + const next = jest.fn(); + await fn(req, null, next); + + expect(req.brew).toEqual(testBrew); + expect(next).toHaveBeenCalled(); + expect(api.getId).toHaveBeenCalledWith(req); + expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); + }); + + it('should handle mongoose error', async ()=>{ + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew'))); + + const fn = api.getBrew('share', true); + const req = { brew: {} }; + const next = jest.fn(); + let err; + try { + await fn(req, null, next); + } catch (e) { + err = e; + } + + expect(err).toEqual(notFoundError); + expect(req.brew).toEqual({}); + expect(next).not.toHaveBeenCalled(); + expect(api.getId).toHaveBeenCalledWith(req); + expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); + }); + + it('changes tags from string to array', async ()=>{ + const testBrew = { title: 'test brew', authors: [], tags: 'tag' }; + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>toBrewPromise(testBrew)); + + const fn = api.getBrew('share', true); + const req = { brew: {} }; + const next = jest.fn(); + await fn(req, null, next); + + expect(req.brew.tags).toEqual([]); + expect(next).toHaveBeenCalled(); + }); + + it('throws if invalid author', async ()=>{ + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] })); + + const fn = api.getBrew('edit', true); + const req = { brew: {} }; + + let err; + try { + await fn(req, null, null); + } catch (e) { + err = e; + } + + expect(err).toEqual('Current logged in user does not have access to this brew.'); + }); + + it('does not throw if no authors', async ()=>{ + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] })); + + const fn = api.getBrew('edit', true); + const req = { brew: {} }; + const next = jest.fn(); + await fn(req, null, next); + + expect(next).toHaveBeenCalled(); + }); + + it('does not throw if valid author', async ()=>{ + api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); + model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] })); + + const fn = api.getBrew('edit', true); + const req = { brew: {}, account: { username: 'a' } }; + const next = jest.fn(); + await fn(req, null, next); + + expect(next).toHaveBeenCalled(); + }); + + it('fetches google brew if needed', async()=>{ + const stubBrew = { title: 'test brew', authors: ['a'] }; + const googleBrew = { title: 'test google brew', text: 'brew text' }; + api.getId = jest.fn(()=>({ id: '1', googleId: '2' })); + model.get = jest.fn(()=>toBrewPromise(stubBrew)); + google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew))); + + const fn = api.getBrew('share', true); + const req = { brew: {} }; + const next = jest.fn(); + await fn(req, null, next); + + expect(req.brew).toEqual({ + title : 'test google brew', + authors : ['a'], + text : 'brew text', + stubbed : true + }); + expect(next).toHaveBeenCalled(); + expect(api.getId).toHaveBeenCalledWith(req); + expect(model.get).toHaveBeenCalledWith({ shareId: '1' }); + expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share'); + }); + }); + + describe('mergeBrewText', ()=>{ + it('should set metadata and no style if it is not present', ()=>{ + const result = api.mergeBrewText({ + text : `brew`, + title : 'some title', + description : 'this is a description', + tags : ['something', 'fun'], + systems : ['D&D 5e'], + renderer : 'v3', + theme : 'phb', + googleId : '12345' + }); + + expect(result).toEqual(`\`\`\`metadata +title: some title +description: this is a description +tags: + - something + - fun +systems: + - D&D 5e +renderer: v3 +theme: phb + +\`\`\` + +brew`); + }); + + it('should set metadata and style', ()=>{ + const result = api.mergeBrewText({ + text : `brew`, + style : 'hello yes i am css', + title : 'some title', + description : 'this is a description', + tags : ['something', 'fun'], + systems : ['D&D 5e'], + renderer : 'v3', + theme : 'phb', + googleId : '12345' + }); + + expect(result).toEqual(`\`\`\`metadata +title: some title +description: this is a description +tags: + - something + - fun +systems: + - D&D 5e +renderer: v3 +theme: phb + +\`\`\` + +\`\`\`css +hello yes i am css +\`\`\` + +brew`); + }); + }); + + describe('exclusion methods', ()=>{ + it('excludePropsFromUpdate removes the correct keys', ()=>{ + const sent = Object.assign({}, googleBrew); + const result = api.excludePropsFromUpdate(sent); + + expect(sent).toEqual(googleBrew); + expect(result._id).toBeUndefined(); + expect(result.views).toBeUndefined(); + expect(result.lastViewed).toBeUndefined(); + expect(result.editId).toBeUndefined(); + expect(result.shareId).toBeUndefined(); + expect(result.googleId).toBeUndefined(); + }); + + it('excludeGoogleProps removes the correct keys', ()=>{ + const sent = Object.assign({}, googleBrew); + const result = api.excludeGoogleProps(sent); + + expect(sent).toEqual(googleBrew); + expect(result.tags).toBeUndefined(); + expect(result.systems).toBeUndefined(); + expect(result.published).toBeUndefined(); + expect(result.authors).toBeUndefined(); + expect(result.owner).toBeUndefined(); + expect(result.views).toBeUndefined(); + expect(result.thumbnail).toBeUndefined(); + }); + + it('excludeStubProps removes the correct keys from the original object', ()=>{ + const sent = Object.assign({}, googleBrew); + const result = api.excludeStubProps(sent); + + expect(sent).not.toEqual(googleBrew); + expect(result.text).toBeUndefined(); + expect(result.textBin).toBeUndefined(); + expect(result.renderer).toBeUndefined(); + expect(result.pageCount).toBeUndefined(); + expect(result.version).toBeUndefined(); + }); + }); + + describe('beforeNewSave', ()=>{ + it('sets the title if none', ()=>{ + const brew = { + ...hbBrew, + title : undefined + }; + api.beforeNewSave({}, brew); + + expect(brew.title).toEqual('brew text'); + }); + + it('does not set the title if present', ()=>{ + const brew = { + ...hbBrew, + title : 'test' + }; + api.beforeNewSave({}, brew); + + expect(brew.title).toEqual('test'); + }); + + it('does not set authors if account missing username', ()=>{ + api.beforeNewSave({}, hbBrew); + + expect(hbBrew.authors).toEqual([]); + }); + + it('sets authors if account has username', ()=>{ + api.beforeNewSave({ username: 'hi' }, hbBrew); + + expect(hbBrew.authors).toEqual(['hi']); + }); + + it('merges brew text', ()=>{ + api.mergeBrewText = jest.fn(()=>'merged'); + api.beforeNewSave({}, hbBrew); + + expect(api.mergeBrewText).toHaveBeenCalled(); + expect(hbBrew.text).toEqual('merged'); + }); + }); + + describe('newGoogleBrew', ()=>{ + it('should call the correct methods', ()=>{ + google.authCheck = jest.fn().mockImplementation(()=>'client'); + api.excludeGoogleProps = jest.fn(()=>'newBrew'); + google.newGoogleBrew = jest.fn(()=>'id'); + + const acct = { username: 'test' }; + const brew = { title: 'test title' }; + const res = { send: jest.fn(()=>{}) }; + api.newGoogleBrew(acct, brew, res); + + expect(google.authCheck).toHaveBeenCalledWith(acct, res); + expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew); + expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew'); + }); + }); +});