diff --git a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js index 5827de782d..b38a0d35c5 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js @@ -4,12 +4,21 @@ import { translate as t, } from '../../../../helpers/api-v3-integration.helper'; import { v4 as generateUUID } from 'uuid'; +import common from '../../../../../common'; describe('GET /groups/:groupId/members', () => { let user; beforeEach(async () => { - user = await generateUser(); + user = await generateUser({ + balance: 10, + contributor: {level: 1}, + backer: {tier: 3}, + preferences: { + costume: false, + background: 'volcano', + }, + }); }); it('validates optional req.query.lastId to be an UUID', async () => { @@ -57,6 +66,30 @@ describe('GET /groups/:groupId/members', () => { expect(res[0].profile).to.have.all.keys(['name']); }); + it('req.query.includeAllPublicFields === true only works with parties', async () => { + let group = await generateGroup(user, {type: 'guild', name: generateUUID()}); + let res = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`); + expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']); + expect(res[0].profile).to.have.all.keys(['name']); + }); + + it('populates all public fields if req.query.includeAllPublicFields === true and it is a party', async () => { + await generateGroup(user, {type: 'party', name: generateUUID()}); + let [memberRes] = await user.get('/groups/party/members?includeAllPublicFields=true'); + + expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party', + 'backer', 'contributor', 'auth', 'items', + ]); + expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); + expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt', + 'chair', 'costume', 'sleep', 'background'].sort()); + + expect(memberRes.stats.maxMP).to.exists; + expect(memberRes.stats.maxHealth).to.equal(common.maxHealth); + expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl)); + }); + it('returns only first 30 members', async () => { let group = await generateGroup(user, {type: 'party', name: generateUUID()}); diff --git a/test/api/v3/integration/hall/GET-hall_heroes.test.js b/test/api/v3/integration/hall/GET-hall_heroes.test.js index 745bc7739c..a2b87ff689 100644 --- a/test/api/v3/integration/hall/GET-hall_heroes.test.js +++ b/test/api/v3/integration/hall/GET-hall_heroes.test.js @@ -1,9 +1,12 @@ import { generateUser, + resetHabiticaDB, } from '../../../../helpers/api-v3-integration.helper'; describe('GET /hall/heroes', () => { it('returns all heroes sorted by -contributor.level and with correct fields', async () => { + await resetHabiticaDB(); + let nonHero = await generateUser(); let hero1 = await generateUser({ contributor: {level: 1}, diff --git a/test/api/v3/integration/members/GET-members_id.test.js b/test/api/v3/integration/members/GET-members_id.test.js index a802a1e8a7..abb98c54b1 100644 --- a/test/api/v3/integration/members/GET-members_id.test.js +++ b/test/api/v3/integration/members/GET-members_id.test.js @@ -3,6 +3,7 @@ import { translate as t, } from '../../../../helpers/api-v3-integration.helper'; import { v4 as generateUUID } from 'uuid'; +import common from '../../../../../common'; describe('GET /members/:memberId', () => { let user; @@ -36,6 +37,10 @@ describe('GET /members/:memberId', () => { expect(Object.keys(memberRes.auth)).to.eql(['timestamps']); expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt', 'chair', 'costume', 'sleep', 'background'].sort()); + + expect(memberRes.stats.maxMP).to.exists; + expect(memberRes.stats.maxHealth).to.equal(common.maxHealth); + expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl)); }); it('handles non-existing members', async () => { diff --git a/test/api/v3/integration/user/GET-user.test.js b/test/api/v3/integration/user/GET-user.test.js index f4ed75f03f..f68c5d59e4 100644 --- a/test/api/v3/integration/user/GET-user.test.js +++ b/test/api/v3/integration/user/GET-user.test.js @@ -1,6 +1,7 @@ import { generateUser, } from '../../../../helpers/api-integration/v3'; +import common from '../../../../../common'; describe('GET /user', () => { let user; @@ -9,9 +10,13 @@ describe('GET /user', () => { user = await generateUser(); }); - it('returns the authenticated user', async () => { + it('returns the authenticated user with computed stats', async () => { let returnedUser = await user.get('/user'); expect(returnedUser._id).to.equal(user._id); + + expect(returnedUser.stats.maxMP).to.exists; + expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth); + expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl)); }); it('does not return private paths (and apiToken)', async () => { diff --git a/test/api/v3/unit/models/user.test.js b/test/api/v3/unit/models/user.test.js index 29ac2c0c07..f6be1a8930 100644 --- a/test/api/v3/unit/models/user.test.js +++ b/test/api/v3/unit/models/user.test.js @@ -1,4 +1,5 @@ import { model as User } from '../../../../../website/server/models/user'; +import common from '../../../../../common'; describe('User Model', () => { it('keeps user._tmp when calling .toJSON', () => { @@ -31,6 +32,21 @@ describe('User Model', () => { expect(toJSON).to.not.have.keys('_nonTmp'); }); + it('can add computed stats to a JSONified user object', () => { + let user = new User(); + let userToJSON = user.toJSON(); + + expect(userToJSON.stats.maxMP).to.not.exists; + expect(userToJSON.stats.maxHealth).to.not.exists; + expect(userToJSON.stats.toNextLevel).to.not.exists; + + user.addComputedStatsToJSONObj(userToJSON); + + expect(userToJSON.stats.maxMP).to.exists; + expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth); + expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl)); + }); + context('notifications', () => { it('can add notifications with data', () => { let user = new User(); diff --git a/website/server/controllers/api-v3/members.js b/website/server/controllers/api-v3/members.js index dbd8a729dc..43d081a481 100644 --- a/website/server/controllers/api-v3/members.js +++ b/website/server/controllers/api-v3/members.js @@ -50,7 +50,10 @@ api.getMember = { if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId})); // manually call toJSON with minimize: true so empty paths aren't returned - res.respond(200, member.toJSON({minimize: true})); + let memberToJSON = member.toJSON({minimize: true}); + member.addComputedStatsToJSONObj(memberToJSON); + + res.respond(200, memberToJSON); }, }; @@ -100,6 +103,7 @@ function _getMembersForItem (type) { let query = {}; let fields = nameFields; + let addComputedStats = false; // add computes stats to the member info when items and stats are available if (type === 'challenge-members') { query.challenges = challenge._id; @@ -111,6 +115,7 @@ function _getMembersForItem (type) { if (req.query.includeAllPublicFields === 'true') { fields = memberFields; + addComputedStats = true; } } } else if (type === 'group-invites') { @@ -138,7 +143,13 @@ function _getMembersForItem (type) { .exec(); // manually call toJSON with minimize: true so empty paths aren't returned - res.respond(200, members.map(member => member.toJSON({minimize: true}))); + let membersToJSON = members.map(member => { + let memberToJSON = member.toJSON({minimize: true}); + if (addComputedStats) member.addComputedStatsToJSONObj(memberToJSON); + + return memberToJSON; + }); + res.respond(200, membersToJSON); }; } diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index abd6d57924..81a70b6d52 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -30,19 +30,14 @@ api.getUser = { middlewares: [authWithHeaders()], url: '/user', async handler (req, res) { - let user = res.locals.user.toJSON(); + let user = res.locals.user; + let userToJSON = user.toJSON(); // Remove apiToken from response TODO make it private at the user level? returned in signup/login - delete user.apiToken; + delete userToJSON.apiToken; - // TODO move to model? (maybe virtuals, maybe in toJSON) - // NOTE: if an item is manually added to user.stats common/fns/predictableRandom must be tweaked - // so it's not considered. Otherwise the client will have it while the server won't and the results will be different. - user.stats.toNextLevel = common.tnl(user.stats.lvl); - user.stats.maxHealth = common.maxHealth; - user.stats.maxMP = common.statsComputed(user).maxMP; - - return res.respond(200, user); + user.addComputedStatsToJSONObj(userToJSON); + return res.respond(200, userToJSON); }, }; diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 9dc794f86b..ea53bf30c3 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -1,4 +1,4 @@ -import shared from '../../../../common'; +import common from '../../../../common'; import Bluebird from 'bluebird'; import { chatDefaults, @@ -23,12 +23,12 @@ schema.methods.getGroups = function getUserGroups () { schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) { let sender = this; - shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender)); + common.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender)); userToReceiveMessage.inbox.newMessages++; userToReceiveMessage._v++; userToReceiveMessage.markModified('inbox.messages'); - shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage))); + common.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage))); sender.markModified('inbox.messages'); let promises = [userToReceiveMessage.save(), sender.save()]; @@ -40,4 +40,15 @@ schema.methods.addNotification = function addUserNotification (type, data = {}) type, data, }); +}; + +// Add stats.toNextLevel, stats.maxMP and stats.maxHealth +// to a JSONified User object +schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (obj) { + // NOTE: if an item is manually added to user.stats then + // common/fns/predictableRandom must be tweaked so the new item is not considered. + // Otherwise the client will have it while the server won't and the results will be different. + obj.stats.toNextLevel = common.tnl(this.stats.lvl); + obj.stats.maxHealth = common.maxHealth; + obj.stats.maxMP = common.statsComputed(this).maxMP; }; \ No newline at end of file