mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
API: return computed stats for members routes (#7870)
* api: return computed stats for members responses * add integration tests for computed stats * add unit tests for computed stats * clarify test name * add missing query parameter to test case * reset test database before running API tests for the Hall
This commit is contained in:
@@ -4,12 +4,21 @@ import {
|
|||||||
translate as t,
|
translate as t,
|
||||||
} from '../../../../helpers/api-v3-integration.helper';
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import common from '../../../../../common';
|
||||||
|
|
||||||
describe('GET /groups/:groupId/members', () => {
|
describe('GET /groups/:groupId/members', () => {
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
beforeEach(async () => {
|
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 () => {
|
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']);
|
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 () => {
|
it('returns only first 30 members', async () => {
|
||||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
|
resetHabiticaDB,
|
||||||
} from '../../../../helpers/api-v3-integration.helper';
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
|
||||||
describe('GET /hall/heroes', () => {
|
describe('GET /hall/heroes', () => {
|
||||||
it('returns all heroes sorted by -contributor.level and with correct fields', async () => {
|
it('returns all heroes sorted by -contributor.level and with correct fields', async () => {
|
||||||
|
await resetHabiticaDB();
|
||||||
|
|
||||||
let nonHero = await generateUser();
|
let nonHero = await generateUser();
|
||||||
let hero1 = await generateUser({
|
let hero1 = await generateUser({
|
||||||
contributor: {level: 1},
|
contributor: {level: 1},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
translate as t,
|
translate as t,
|
||||||
} from '../../../../helpers/api-v3-integration.helper';
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import common from '../../../../../common';
|
||||||
|
|
||||||
describe('GET /members/:memberId', () => {
|
describe('GET /members/:memberId', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -36,6 +37,10 @@ describe('GET /members/:memberId', () => {
|
|||||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||||
'chair', 'costume', 'sleep', 'background'].sort());
|
'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 () => {
|
it('handles non-existing members', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
import common from '../../../../../common';
|
||||||
|
|
||||||
describe('GET /user', () => {
|
describe('GET /user', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -9,9 +10,13 @@ describe('GET /user', () => {
|
|||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the authenticated user', async () => {
|
it('returns the authenticated user with computed stats', async () => {
|
||||||
let returnedUser = await user.get('/user');
|
let returnedUser = await user.get('/user');
|
||||||
expect(returnedUser._id).to.equal(user._id);
|
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 () => {
|
it('does not return private paths (and apiToken)', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
|
import common from '../../../../../common';
|
||||||
|
|
||||||
describe('User Model', () => {
|
describe('User Model', () => {
|
||||||
it('keeps user._tmp when calling .toJSON', () => {
|
it('keeps user._tmp when calling .toJSON', () => {
|
||||||
@@ -31,6 +32,21 @@ describe('User Model', () => {
|
|||||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
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', () => {
|
context('notifications', () => {
|
||||||
it('can add notifications with data', () => {
|
it('can add notifications with data', () => {
|
||||||
let user = new User();
|
let user = new User();
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ api.getMember = {
|
|||||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||||
|
|
||||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
// 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 query = {};
|
||||||
let fields = nameFields;
|
let fields = nameFields;
|
||||||
|
let addComputedStats = false; // add computes stats to the member info when items and stats are available
|
||||||
|
|
||||||
if (type === 'challenge-members') {
|
if (type === 'challenge-members') {
|
||||||
query.challenges = challenge._id;
|
query.challenges = challenge._id;
|
||||||
@@ -111,6 +115,7 @@ function _getMembersForItem (type) {
|
|||||||
|
|
||||||
if (req.query.includeAllPublicFields === 'true') {
|
if (req.query.includeAllPublicFields === 'true') {
|
||||||
fields = memberFields;
|
fields = memberFields;
|
||||||
|
addComputedStats = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === 'group-invites') {
|
} else if (type === 'group-invites') {
|
||||||
@@ -138,7 +143,13 @@ function _getMembersForItem (type) {
|
|||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
// 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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,19 +30,14 @@ api.getUser = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
url: '/user',
|
url: '/user',
|
||||||
async handler (req, res) {
|
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
|
// 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)
|
user.addComputedStatsToJSONObj(userToJSON);
|
||||||
// NOTE: if an item is manually added to user.stats common/fns/predictableRandom must be tweaked
|
return res.respond(200, userToJSON);
|
||||||
// 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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import shared from '../../../../common';
|
import common from '../../../../common';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import {
|
import {
|
||||||
chatDefaults,
|
chatDefaults,
|
||||||
@@ -23,12 +23,12 @@ schema.methods.getGroups = function getUserGroups () {
|
|||||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
|
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
|
||||||
let sender = this;
|
let sender = this;
|
||||||
|
|
||||||
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
common.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
|
||||||
userToReceiveMessage.inbox.newMessages++;
|
userToReceiveMessage.inbox.newMessages++;
|
||||||
userToReceiveMessage._v++;
|
userToReceiveMessage._v++;
|
||||||
userToReceiveMessage.markModified('inbox.messages');
|
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');
|
sender.markModified('inbox.messages');
|
||||||
|
|
||||||
let promises = [userToReceiveMessage.save(), sender.save()];
|
let promises = [userToReceiveMessage.save(), sender.save()];
|
||||||
@@ -40,4 +40,15 @@ schema.methods.addNotification = function addUserNotification (type, data = {})
|
|||||||
type,
|
type,
|
||||||
data,
|
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;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user