From d79e0f6e2ea8d63e72a0752bbc75092d795b5cf5 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 18 Feb 2016 19:41:35 +0100 Subject: [PATCH] add tests for hall --- .../integration/hall/GET-hall_heroes.test.js | 29 ++++ .../hall/GET-hall_heroes_heroId.test.js | 56 +++++++ .../integration/hall/GET-hall_patrons.test.js | 60 +++++++ .../hall/PUT-hall_heores_heroId.test.js | 148 ++++++++++++++++++ website/src/controllers/api-v3/hall.js | 99 ++++++------ 5 files changed, 346 insertions(+), 46 deletions(-) create mode 100644 test/api/v3/integration/hall/GET-hall_heroes.test.js create mode 100644 test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js create mode 100644 test/api/v3/integration/hall/GET-hall_patrons.test.js create mode 100644 test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js diff --git a/test/api/v3/integration/hall/GET-hall_heroes.test.js b/test/api/v3/integration/hall/GET-hall_heroes.test.js new file mode 100644 index 0000000000..745bc7739c --- /dev/null +++ b/test/api/v3/integration/hall/GET-hall_heroes.test.js @@ -0,0 +1,29 @@ +import { + generateUser, +} from '../../../../helpers/api-v3-integration.helper'; + +describe('GET /hall/heroes', () => { + it('returns all heroes sorted by -contributor.level and with correct fields', async () => { + let nonHero = await generateUser(); + let hero1 = await generateUser({ + contributor: {level: 1}, + }); + let hero2 = await generateUser({ + contributor: {level: 3}, + }); + + let heroes = await nonHero.get('/hall/heroes'); + expect(heroes.length).to.equal(2); + expect(heroes[0]._id).to.equal(hero2._id); + expect(heroes[1]._id).to.equal(hero1._id); + + expect(heroes[0]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']); + expect(heroes[1]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']); + + expect(heroes[0].profile).to.have.all.keys(['name']); + expect(heroes[1].profile).to.have.all.keys(['name']); + + expect(heroes[0].profile.name).to.equal(hero2.profile.name); + expect(heroes[1].profile.name).to.equal(hero1.profile.name); + }); +}); diff --git a/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js new file mode 100644 index 0000000000..ba2bec1783 --- /dev/null +++ b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js @@ -0,0 +1,56 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +describe('GET /heroes/:heroId', () => { + let user; + + before(async () => { + user = await generateUser({ + contributor: {admin: true}, + }); + }); + + it('requires the caller to be an admin', async () => { + let nonAdmin = await generateUser(); + + await expect(nonAdmin.get(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('noAdminAccess'), + }); + }); + + it('validates req.params.heroId', async () => { + await expect(user.get(`/hall/heroes/invalidUUID`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('handles non-existing heroes', async () => { + let dummyId = generateUUID(); + await expect(user.get(`/hall/heroes/${dummyId}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithIDNotFound', {userId: dummyId}), + }); + }); + + it('returns only necessary hero data', async () => { + let hero = await generateUser({ + contributor: {tier: 23}, + }); + let heroRes = await user.get(`/hall/heroes/${hero._id}`); + + expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'balance', 'profile', 'purchased', + 'contributor', 'auth', 'items', + ]); + expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); + expect(heroRes.profile).to.have.all.keys(['name']); + }); +}); diff --git a/test/api/v3/integration/hall/GET-hall_patrons.test.js b/test/api/v3/integration/hall/GET-hall_patrons.test.js new file mode 100644 index 0000000000..17fa3fd58d --- /dev/null +++ b/test/api/v3/integration/hall/GET-hall_patrons.test.js @@ -0,0 +1,60 @@ +import { + generateUser, + translate as t, + resetHabiticaDB, +} from '../../../../helpers/api-v3-integration.helper'; +import { times } from 'lodash'; + +describe('GET /hall/patrons', () => { + let user; + + beforeEach(async () => { + await resetHabiticaDB(); + user = await generateUser(); + }); + + it('fails if req.query.page is not numeric', async () => { + await expect(user.get(`/hall/patrons?page=notNumber`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('returns all patrons sorted by -backer.tier and with correct fields', async () => { + let patron1 = await generateUser({ + backer: {tier: 1}, + }); + let patron2 = await generateUser({ + backer: {tier: 3}, + }); + + let patrons = await user.get('/hall/patrons'); + expect(patrons.length).to.equal(2); + expect(patrons[0]._id).to.equal(patron2._id); + expect(patrons[1]._id).to.equal(patron1._id); + + expect(patrons[0]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']); + expect(patrons[1]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']); + + expect(patrons[0].profile).to.have.all.keys(['name']); + expect(patrons[1].profile).to.have.all.keys(['name']); + + expect(patrons[0].profile.name).to.equal(patron2.profile.name); + expect(patrons[1].profile.name).to.equal(patron1.profile.name); + }); + + it('returns only first 50 patrons per request, more if req.query.page is passed', async () => { + await Promise.all(times(53, n => { + return generateUser({backer: {tier: n}}); + })); + + let patrons = await user.get('/hall/patrons'); + expect(patrons.length).to.equal(50); + + let morePatrons = await user.get('/hall/patrons?page=1'); + expect(morePatrons.length).to.equal(2); + expect(morePatrons[0].backer.tier).to.equal(2); + expect(morePatrons[1].backer.tier).to.equal(1); + }); +}); diff --git a/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js new file mode 100644 index 0000000000..391d5cdeee --- /dev/null +++ b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js @@ -0,0 +1,148 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-v3-integration.helper'; +import { v4 as generateUUID } from 'uuid'; + +describe('PUT /heroes/:heroId', () => { + let user; + + before(async () => { + user = await generateUser({ + contributor: {admin: true}, + }); + }); + + it('requires the caller to be an admin', async () => { + let nonAdmin = await generateUser(); + + await expect(nonAdmin.put(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('noAdminAccess'), + }); + }); + + it('validates req.params.heroId', async () => { + await expect(user.put(`/hall/heroes/invalidUUID`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('handles non-existing heroes', async () => { + let dummyId = generateUUID(); + await expect(user.put(`/hall/heroes/${dummyId}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userWithIDNotFound', {userId: dummyId}), + }); + }); + + it('updates contributor level, balance, ads, blocked', async () => { + let hero = await generateUser(); + let heroRes = await user.put(`/hall/heroes/${hero._id}`, { + balance: 3, + contributor: {level: 1}, + purchased: {ads: true}, + auth: {blocked: true}, + }); + + // test response + expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'balance', 'profile', 'purchased', + 'contributor', 'auth', 'items', + ]); + expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); + expect(heroRes.profile).to.have.all.keys(['name']); + + // test response values + expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level + expect(heroRes.contributor.level).to.equal(1); + expect(heroRes.purchased.ads).to.equal(true); + expect(heroRes.auth.blocked).to.equal(true); + // test hero values + await hero.sync(); + expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level + expect(hero.contributor.level).to.equal(1); + expect(hero.flags.contributor).to.equal(true); + expect(hero.purchased.ads).to.equal(true); + expect(hero.auth.blocked).to.equal(true); + }); + + it('updates contributor level', async () => { + let hero = await generateUser({ + contributor: {level: 5}, + }); + let heroRes = await user.put(`/hall/heroes/${hero._id}`, { + contributor: {level: 6}, + }); + + // test response + expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'balance', 'profile', 'purchased', + 'contributor', 'auth', 'items', + ]); + expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); + expect(heroRes.profile).to.have.all.keys(['name']); + + // test response values + expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level + expect(heroRes.contributor.level).to.equal(6); + expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5); + // test hero values + await hero.sync(); + expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level + expect(hero.contributor.level).to.equal(6); + expect(hero.flags.contributor).to.equal(true); + expect(hero.items.pets['Dragon-Hydra']).to.equal(5); + }); + + it('updates contributor data', async () => { + let hero = await generateUser({ + contributor: {level: 5}, + }); + let heroRes = await user.put(`/hall/heroes/${hero._id}`, { + contributor: {text: 'Astronaut'}, + }); + + // test response + expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'balance', 'profile', 'purchased', + 'contributor', 'auth', 'items', + ]); + expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); + expect(heroRes.profile).to.have.all.keys(['name']); + + // test response values + expect(heroRes.contributor.level).to.equal(5); // doesn't modify previous values + expect(heroRes.contributor.text).to.equal('Astronaut'); + // test hero values + await hero.sync(); + expect(hero.contributor.level).to.equal(5); // doesn't modify previous values + expect(hero.contributor.text).to.equal('Astronaut'); + }); + + it('updates items', async () => { + let hero = await generateUser(); + let heroRes = await user.put(`/hall/heroes/${hero._id}`, { + itemPath: 'items.special.snowball', + itemVal: 5, + }); + + // test response + expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'balance', 'profile', 'purchased', + 'contributor', 'auth', 'items', + ]); + expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); + expect(heroRes.profile).to.have.all.keys(['name']); + + // test response values + expect(heroRes.items.special.snowball).to.equal(5); + // test hero values + await hero.sync(); + expect(hero.items.special.snowball).to.equal(5); + }); +}); diff --git a/website/src/controllers/api-v3/hall.js b/website/src/controllers/api-v3/hall.js index c1afb806aa..57454c038d 100644 --- a/website/src/controllers/api-v3/hall.js +++ b/website/src/controllers/api-v3/hall.js @@ -9,6 +9,44 @@ import _ from 'lodash'; let api = {}; +/** + * @api {get} /hall/patrons Get all Patrons. Only the first 50 patrons are returned. More can be accessed passing ?page=n. + * @apiVersion 3.0.0 + * @apiName GetPatrons + * @apiGroup Hall + * + * @apiParam {Number} page The result page. Default is 0 + * + * @apiSuccess {Array} patron An array of patrons + */ +api.getPatrons = { + method: 'GET', + url: '/hall/patrons', + middlewares: [authWithHeaders(), cron], + async handler (req, res) { + req.checkQuery('page', res.t('pageMustBeNumber')).optional().isNumeric(); + + let validationErrors = req.validationErrors(); + if (validationErrors) throw validationErrors; + + let page = req.query.page ? Number(req.query.page) : 0; + const perPage = 50; + + let patrons = await User + .find({ + 'backer.tier': {$gt: 0}, + }) + .select('contributor backer profile.name') + .sort('-backer.tier') + .skip(page * perPage) + .limit(perPage) + .lean() + .exec(); + + res.respond(200, patrons); + }, +}; + /** * @api {get} /hall/heroes Get all Heroes * @apiVersion 3.0.0 @@ -26,7 +64,7 @@ api.getHeroes = { .find({ 'contributor.level': {$gt: 0}, }) - .select('contributor backer balance profile.name') + .select('contributor backer profile.name') .sort('-contributor.level') .lean() .exec(); @@ -39,7 +77,7 @@ api.getHeroes = { // they can be used by admins to get/update any user // TODO rename? -const heroAdminFields = 'contributor balance profile.name purchased items auth.local.username auth'; +const heroAdminFields = 'contributor balance profile.name purchased items auth'; /** * @api {get} /hall/heroes/:heroId Get an hero given his _id. Must be an admin to make this request @@ -69,11 +107,14 @@ api.getHero = { let hero = await User .findById(heroId) .select(heroAdminFields) - .lean() .exec(); if (!hero) throw new NotFound(res.t('userWithIDNotFound', {userId: heroId})); - res.respond(200, hero); + let heroRes = hero.toJSON({minimize: true}); + // supply to the possible absence of hero.contributor + // if we didn't pass minimize: true it would have returned all fields as empty + if (!heroRes.contributor) heroRes.contributor = {}; + res.respond(200, heroRes); }, }; @@ -112,7 +153,7 @@ api.updateHero = { if (updateData.balance) hero.balance = updateData.balance; // give them gems if they got an higher level - let newTier = updateData.contributor.level; // tier = level in this context + let newTier = updateData.contributor && updateData.contributor.level; // tier = level in this context let oldTier = hero.contributor && hero.contributor.level || 0; if (newTier > oldTier) { hero.flags.contributor = true; @@ -124,8 +165,9 @@ api.updateHero = { } } - if (updateData.contributor) hero.contributor = updateData.contributor; + if (updateData.contributor) _.assign(hero.contributor, updateData.contributor); if (updateData.purchased && updateData.purchased.ads) hero.purchased.ads = updateData.purchased.ads; + // give them the Dragon Hydra pet if they're above level 6 if (hero.contributor.level >= 6) hero.items.pets['Dragon-Hydra'] = 5; if (updateData.itemPath && updateData.itemVal && @@ -133,53 +175,18 @@ api.updateHero = { User.schema.paths[updateData.itemPath]) { _.set(hero, updateData.itemPath, updateData.itemVal); // Sanitization at 5c30944 (deemed unnecessary) TODO review } + if (updateData.auth && _.isBoolean(updateData.auth.blocked)) hero.auth.blocked = updateData.auth.blocked; let savedHero = await hero.save(); - let responseHero = {}; // only respond with important fields - heroAdminFields.split().forEach(field => { - _.set(responseHero, field, _.get(savedHero, field)); + let heroJSON = savedHero.toJSON(); + let responseHero = {_id: heroJSON._id}; // only respond with important fields + heroAdminFields.split(' ').forEach(field => { + _.set(responseHero, field, _.get(heroJSON, field)); }); res.respond(200, responseHero); }, }; -/** - * @api {get} /hall/patrons Get all Patrons. Only the first 50 patrons are returned. More can be accessed passing ?page=n. - * @apiVersion 3.0.0 - * @apiName GetPatrons - * @apiGroup Hall - * - * @apiParam {Number} page The result page. Default is 0 - * - * @apiSuccess {Array} patron An array of patrons - */ -api.getPatrons = { - method: 'GET', - url: '/hall/patrons', - middlewares: [authWithHeaders(), cron], - async handler (req, res) { - req.checkQuery('page', res.t('pageMustBeNumber')).isNumeric(); - - let validationErrors = req.validationErrors(); - if (validationErrors) throw validationErrors; - - let page = req.query.page || 0; - const perPage = 50; - - let patrons = await User - .find({ - 'backer.tier': {$gt: 0}, - }) - .select('contributor backer profile.name') - .sort('-backer.tier') - .skip(page * perPage) - .limit(perPage) - .exec(); - - res.respond(200, patrons); - }, -}; - export default api;