API v3 [WIP] (#6144)

* Fixed more tests

* Added tags into user service

* Added api-v3 auth urls

* v3: fix package.json

* v3: fix package.json

* Fixed auth tests. Updated Authctrl response

* v3: remove newrelic config file in favour of env variables

* v3: upgrade some deps

* switch from Q to Bluebird

* v3 fix tests with deferred

* Removed extra consoles.log. Changed data.data to res.data

* v3 fix tests and use coroutines instead of regenerator

* v3: fix tests

* v3: do not await a non promise

* v3: q -> bluebird

* Changed id param for registration response

* Updated party query and create

* Ensured login callback happens after user sync

* Add challenges to groups. Fixed isMemberOfGuild check

* Updated party and group tests

* Fixed cron test

* return user.id and send analytics event before changing page

* fix trailing spaces

* disable redirects

* Api v3 party tavern fixes (#7191)

* Added check if user is in party before query

* Cached party query. Prevented party request when user is not in party. Updated Party create with no invites

* Update tavern ctrl to use new promise

* v3: misc fixes

* Api v3 task fixes (#7193)

* Update task view to use _id

* Added try catch to user service ops calls

* v3 client: saving after syncing is complete

* Fixed test broken by part sync change (#7195)

* v3: fix todo scoring and try to fix production testing problem

* revert changes to mongoose config

* mongoose: increase keepAlive

* test mongoose fix

* fix: Only apply captureStackTrace if it exists on the error object

* v3: fix reminders with no startDate

* mongoose: use options

* chore(): rename website/src -> website/server and website/public -> website/client (#7199)

* v3 fix GET /groups: return an error only if an invalid type is supplied not when there are 0 results (#7203)

* [API v3] Fix calls to user.ops and deleting tags (#7204)

* v3: fixes calls to user.ops from views and deleting tags

* v3: fix tests that use user._statsComputed

* Api v3 fixes continued (#7205)

* Added timzeone offset back

* Added APIToken back to settings page

* Fixed fetch recent messages for party

* Fixed returning group description

* Fixed check if user is member of challenge

* Fixed party members appearing in header

* Updated get myGroups param to include public groups. Fixed isMemberOf group

* Fixed hourglass purchase

* Fixed challenge addding tasks on first creating

* Updated tests to accomidate new changes

* fix: Correct checklist on client

Closes #7207

* fix: Pin eslint to 2.9

* minor improvements to cron code for clarity; fix inaccurate comments; add TODOs for rest-in-inn actions

* fix: Add missing type param to equip call

closes #7212

* rename and reword pubChalsMinPrize to reflect that it's only for Tavern challenges

* allows players to send gems to each other; other minor related changes - fixes https://github.com/HabitRPG/habitrpg/issues/7227

* fix tests for /members/transfer-gems

* fix: Set gems sent notification as translatable string

* chore: Remove unusued variable

* fix: Remove requirement on message paramter in transfer-gems

* add a missing variable declaration

* chore: clarify comments on cron code

* fix: Correct client request from habitrpg -> tavern

* update apidoc URL in package.json

Closes #7222

* Fixed start party by invites

* Updated spell casting to v3

* Fixed adding and removing tags on tasks

* Fixed page reload on settings change

* Fixed battle monsters with friends button

* Loaded completed todos when done is clicked

* chore: Reinstate floating version number for eslint

babel-eslint regression fixed

* Fixed reload tests

* change "an user" to "a user" in comments and text (no code changes) (#7257)

* fix: Alert user that drops were recieved

* remove userServices.js from karma.conf - it's been moved to website/client/js/services

* feat: Create debug update user route

* fix: Correct set cron debug function

* feat: Add make admin button to debug menu

* lint: Add missing semicolons in test

* fix: Temporarilly comment out udpate user debug route

* v3: fix _tmp for crit and streakBonus

* v3: execute all actions when leaving a solo party

* v3 client: fix group not found when leaving party

* v3 migration: fix challenge prize

* v3 cron: only save modified tasks

* v3: add CHALLENGE_TASK_NOT_FOUND to valid broken reasons

* v3: fix tasks chart

* v3 client: fix ability to leave challenge

* v3 client: fix filtering by tag and correctly show tag tooltip

* v3 common: fix tags tests

* v3 client: support unlinking not found challenges tasks

* v3: disable Bluebird warning for missing return, fixes #7269

* feat: Separate out update-user into set-cron and make-admin debug routes

* chore: Disable make admin debug route for v3 prod testing

* v3: misc fixes

* v3: misc fixes

* v3: fix adding multiple tasks

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* v3 client: fix casting spells

* v3: do not log sensitive data

* v3: always save user when casting spell

* v3: always save user when casting spell

* v3: more fixes for spells

* fix typos and missing information in apidocs - fixes https://github.com/HabitRPG/habitrpg/issues/7277 (#7282)

* v3: add TODO for client side spells

* feat: Add modify inventory debug menu

* Fixed viewing user progress on challenge

* Updated tests

* fix: Fix quest progress button

* fix incorrect Armoire test; remove unneeded param details from apidocs; disambiguate health potion

* v3: fix stealth casting

* v3: fix tasks saving and selection for rebirth reroll and reset (server-only)

* v3: fix auto allocation

* v3 client: misc fixes

* rename buyPotion and buy-potion to buyHealthPotion and buy-health-potion; fix apidoc param error

* Added delete for saved challenge task

* Fixed member modal on front page

* adjust text in apidocs for errors / clarity / consistency / standard terminology (no code changes) (#7298)

* fix bug in Rebirth test, add new tests, adjust apidocs (#7293)

* Updated task model to allow setting streak (#7306)

* fix: Correct missing * in apidoc comments

* Api v3 challenge fixes (#7287)

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* Fixed viewing user progress on challenge

* Updated tests

* Added delete for saved challenge task

* v3: fix sorting

* [API v3] add CRON_SAFE_MODE (#7286)

* add CRON_SAFE_MODE to example config file, fix some bugs, add an unrelated low-priority TODO

* create CRON_SAFE_MODE to disable parts of cron for use after extended outage - fixes https://github.com/HabitRPG/habitrpg/issues/7161

* fix a bug with CRON_SAFE_MODE, remove duplicated code, remove completed TODO comment

* fix check for CRON_SAFE_MODE

* v3 client: fix typo

* adjust debug menu Modify Inventory: hungrier pets, fewer Special items, "Hide" buttons

* completed To-Dos: return the 30 most recent instead of 30 oldest (#7318)

* v3 migration: fix createdAt date

* adjust locales text, key names, and files for Rebirth, Reset, and Fortify / ReRoll for consistency with existing strings (#7321)

* v3: fix unlinking multiple tasks

* v3 fix releasing pets

* v3: fix authenticating with apiUrl

* v3: fix typo

* v3 fix client tests for unlinking

* v3 client: do not show start quest button when quest is active

* v3 client: fix ability to send cards

* v3 client: fix misc challenge issues

* v3: fix notifications

* v3 client: more user friendly errors

* v3 client: only load completed todos once

* v3 client: fix tests

* v3: move TAVERN_ID to common code

* fix: Provide default type and text for new task creation in score route

* fix: Provide default history [] for habit in score route

* fix: Add _legacyId prop to tasks to support non-uuid identifiers

* chore: Change v3 migration to use _legacyId instead of legacyId

* fix: check for _legacyId in tasks if id does not exist

* refactor: Extract out finding task by id or _legacyId into a function

* Api v3 party quest fixes (#7341)

* Fix display of add challenge message when group challenges are empty

* Fixed forced quest start to update quest without reload

* Fixed needing to reload when accepting party invite

* Fix group leave and join reload

* Fixed leave current party and join another

* Updated party tests

* v3 client: remove console.log statement

* v3: misc fixes

* v3 client: fix predicatbale random

* v3: info about API v3

* v3: update footer with links to developer resources

* v3: support party invitation from email

* v3 client: fix chat flagging

* fix: Correct get tasks route to properly get todos (#7349)

* move locales strings from api-v3.json to other locales files (#7347)

* move locales strings from api-v3.json: authentication strings -> front.json

* move locales strings from api-v3.json: authentication strings -> tasks.json

* move locales strings from api-v3.json: authentication strings -> groups.json

* move locales strings from api-v3.json: authentication strings -> challenge.json

* move locales strings from api-v3.json: authentication strings -> groups.json (again)

* move locales strings from api-v3.json: authentication strings -> quests.json

* move locales strings from api-v3.json: authentication strings -> subscriber.json

* move locales strings from api-v3.json: authentication strings -> spells.json

* move locales strings from api-v3.json: authentication strings -> character.json

* move locales strings from api-v3.json: authentication strings -> groups.json (PMs)

* move locales strings from api-v3.json: authentication strings -> npc.json

* move locales strings from api-v3.json: authentication strings -> pets.json

* move locales strings from api-v3.json: authentication strings -> miscellaneous

* move locales strings from api-v3.json: authentication strings -> contrib.json and settings.json

* move locales strings from api-v3.json: delete unused string (invalidTasksOwner), delete api-v3.json, whitespace cleanup

* v3 client: fix sticky header

* v3: remove unused code

* v3 client: correctly redirect after inviting

* Removed v2 calls from views (#7351)

* v3: fix tests for challenge export

* v3: fallbackto authWithHeaders if wuthWithSession or authWithUrl fails

* Added force cache update when fetching new messages (#7360)

* v3: fetch whole user when booting from group tto avoid issues with pre save hook expecting all data

* v3: misc fixes for payments

* v3: limit fields of challenge tasks that can be updated

* fix(tests): never connect to NODE_DB_URI for tests

* Added new route for setting last cron and updated front end

* v3: fix iap url

* v3: fix build and ios IAP

* Changed route to user set custom day start

* v3: iap accessible under /api/v3, fixes to spells and groups invitations

* v3: correctly use v3 routes in client

* remove XP, GP when unticking a Daily with a completed checklist - fixes https://github.com/HabitRPG/habitrpg/issues/7246

* use natural language for error message about skills on challenge tasks (#7336), fix other gramatical error

* Updated ui when user rejects a guild invite (#7368)

* feat: complete custom day start route

Closes #7363

* fix: Correct spelling of healAll skill

fix: Correct sprite name of healAll skill

* fix: Change all instances of spookDust -> spookySparkles

* add dateCreated to all tasks; add empty challenge object to tasks that don't have one (#7386)

* add plumilla to artists for Tangle Tree in Bailey message

* Fixed quest drop modal (#7377)

* Fixed quest drop modal

* Fixed broken party test

* [API v3] Maintenance Mode (#7367)

* WIP(maintenance): maintenance

* WIP(maintenance): working locale features

* fix(maintenance): don't translate info page target

* WIP(maintenance): start adding info page

* fix(maintenance): linting

* feat: Add container to maintenance info page

* fix(maintenance): add config.json edits
Also DRY variables for main vs info pages

* fix(maintenance): linting

* refactor(maintenance): further slim down variables

* refactor: Remove unnecessary variables

* fix: Correct string interpolation in maintenace view

* feat: Dynamically add time to maintenance pages

* maintenance mode: do not connect to mongodb

* fix(maintenance): clean up timezones etc.

* fix(maintenance): remove unneeded sprite

* Tavern party challenges invites fix (#7394)

* Added challenges and invitations to party

* Loaded tavern challenges

* Updated group and quest services tests

* v3: implement automatic syncing if user is not up to date

* Removed unnecessary fields when updating groups and challenges (#7395)

* v3: do not saved populated user

* v3: correctly return user subset

* Chained party promises together (#7396)

* v3: $w -> splitWhitespace

* use bluebird

* use babel polyfill

* migration: fix items

* update links for v3

* Updated shortname validation to support multiple browsers

* Docs changes (#7401)

* chore: Clarify transfer-gems documentation

* chore: Clarify api status route documentation

* chore: Mark webhooks as BETA

* Added tags update route. Added sort to user service (#7381)

* Added tags update route. Added sort to user service

* Change update tasks route to reorder tasks

* Fixed linting issue

* Changed params for reorder tags route

* Fixed not found tag and added test

* Added password confirmation when deleteing account (#7402)

* fix production logging

* feat(commit): push

* empty commit

* feat(maintenance): post-downtime news & awards (#7406)

* fix exporting avatar

* second attempt at fixing exporting avatar

* fix production logging

* s3: convert moment to date instance

* fix avatar sharing and caching (30 minutes)

* fix: Correct missing parameter

Closes #7433

* fix: Validate challenge shortname on server

* adjust text strings - fixes https://github.com/HabitRPG/habitrpg/issues/5631 and also Short Name -> Tag Name
This commit is contained in:
Matteo Pagliazzi
2016-05-23 13:58:31 +02:00
parent ef3a2fc286
commit 28f2e9c356
993 changed files with 44888 additions and 12883 deletions

View File

@@ -0,0 +1,94 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
sleep,
checkExistence,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /challenges/:challengeId', () => {
it('returns error when challengeId is not a valid UUID', async () => {
let user = await generateUser();
await expect(user.del('/challenges/test')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when challengeId is not for a valid challenge', async () => {
let user = await generateUser();
await expect(user.del(`/challenges/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
context('Deleting a valid challenge', () => {
let groupLeader;
let group;
let challenge;
let taskText = 'A challenge task text';
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup();
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
]);
await challenge.sync();
});
it('returns an error when user doesn\'t have permissions to delete the challenge', async () => {
let user = await generateUser();
await expect(user.del(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyLeaderDeleteChal'),
});
});
it('deletes challenge', async () => {
await groupLeader.del(`/challenges/${challenge._id}`);
await sleep(0.5);
await expect(checkExistence('challenges', challenge._id)).to.eventually.equal(false);
});
it('refunds gems to group leader', async () => {
let oldBalance = (await groupLeader.sync()).balance;
await groupLeader.del(`/challenges/${challenge._id}`);
await sleep(0.5);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4);
});
it('sets broken and doesn\'t set winner flags for user\'s challenge tasks', async () => {
await groupLeader.del(`/challenges/${challenge._id}`);
await sleep(0.5);
let tasks = await groupLeader.get('/tasks/user');
let testTask = _.find(tasks, (task) => {
return task.text === taskText;
});
expect(testTask.challenge.broken).to.eql('CHALLENGE_DELETED');
expect(testTask.challenge.winner).to.be.null;
});
});
});

View File

@@ -0,0 +1,142 @@
import {
generateUser,
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId', () => {
it('fails if challenge doesn\'t exists', async () => {
let user = await generateUser();
await expect(user.get(`/challenges/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
context('public guild', () => {
let groupLeader;
let group;
let challenge;
let user;
beforeEach(async () => {
user = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'public'},
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
});
it('should return challenge data', async () => {
let chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
expect(chal.group).to.eql(_.pick(group, ['_id', 'id', 'name', 'type', 'privacy']));
});
});
context('private guild', () => {
let groupLeader;
let group;
let challenge;
let members;
let user;
beforeEach(async () => {
user = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'private'},
members: 1,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
expect(chal.group).to.eql(_.pick(group, ['_id', 'id', 'name', 'type', 'privacy']));
});
});
context('party', () => {
let groupLeader;
let group;
let challenge;
let members;
let user;
beforeEach(async () => {
user = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party'},
members: 1,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader.id,
profile: {name: groupLeader.profile.name},
});
expect(chal.group).to.eql(_.pick(group, ['_id', 'id', 'name', 'type', 'privacy']));
});
});
});

View File

@@ -0,0 +1,74 @@
import {
generateUser,
createAndPopulateGroup,
generateChallenge,
translate as t,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/export/csv', () => {
let groupLeader;
let group;
let challenge;
let members;
let user;
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 3,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await members[1].post(`/challenges/${challenge._id}/join`);
await members[2].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'Task 1'},
{type: 'todo', text: 'Task 2'},
]);
await sleep(0.5); // Make sure tasks are synced to the users
await members[0].sync();
await members[1].sync();
await members[2].sync();
});
it('fails if challenge doesn\'t exists', async () => {
user = await generateUser();
user.get('/user');
await expect(user.get(`/challenges/${generateUUID()}/export/csv`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
user = await generateUser();
user.get('/user');
await expect(user.get(`/challenges/${challenge._id}/export/csv`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return a valid CSV file with export data', async () => {
let res = await members[0].get(`/challenges/${challenge._id}/export/csv`);
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
let splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Task,Value,Notes');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[5]).to.equal('');
});
});

View File

@@ -0,0 +1,109 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('validates optional req.query.lastId to be an UUID', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members?lastId=invalidUUID`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails if challenge doesn\'t exists', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user);
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
await expect(anotherUser.get(`/challenges/${challenge._id}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('works with challenges belonging to public guild', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(leader, group);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: leader._id,
id: leader._id,
profile: {name: leader.profile.name},
});
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('populates only some fields', async () => {
let anotherUser = await generateUser({balance: 3});
let group = await generateGroup(anotherUser, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(anotherUser, group);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
id: anotherUser._id,
profile: {name: anotherUser.profile.name},
});
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('returns only first 30 members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
}
await Promise.all(usersToGenerate);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
it('supports using req.query.lastId to get more members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let usersToGenerate = [];
for (let i = 0; i < 57; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
}
let generatedUsers = await Promise.all(usersToGenerate); // Group has 59 members (1 is the leader)
let expectedIds = [user._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res.length).to.equal(30);
let res2 = await user.get(`/challenges/${challenge._id}/members?lastId=${res[res.length - 1]._id}`);
expect(res2.length).to.equal(28);
let resIds = res.concat(res2).map(member => member._id);
expect(resIds).to.eql(expectedIds.sort());
});
});

View File

@@ -0,0 +1,107 @@
import {
generateUser,
generateChallenge,
generateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/members/:memberId', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('validates req.params.memberId to be an UUID', async () => {
await expect(user.get(`/challenges/invalidUUID/members/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('validates req.params.memberId to be an UUID', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members/invalidUUID`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails if member doesn\'t exists', async () => {
let userId = generateUUID();
await expect(user.get(`/challenges/${generateUUID()}/members/${userId}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId}),
});
});
it('fails if challenge doesn\'t exists', async () => {
let member = await generateUser();
await expect(user.get(`/challenges/${generateUUID()}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
let member = await generateUser();
await expect(anotherUser.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('fails if member is not part of the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let member = await generateUser();
await expect(user.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeMemberNotFound'),
});
});
it('works with challenges belonging to a public guild', async () => {
let groupLeader = await generateUser({balance: 4});
let group = await generateGroup(groupLeader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(groupLeader, group);
let taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});
it('returns the member tasks for the challenges', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: 'Test Text'}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
let chalTasks = await user.get(`/tasks/challenge/${challenge._id}`);
expect(memberProgress.tasks.length).to.equal(chalTasks.length);
expect(memberProgress.tasks[0].challenge.id).to.equal(challenge._id);
expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id);
});
it('returns the tasks without the tags', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let taskText = 'Test Text';
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
expect(memberProgress.tasks[0]).not.to.have.key('tags');
});
});

View File

@@ -0,0 +1,118 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET challenges/group/:groupId', () => {
context('Public Guild', () => {
let publicGuild, user, nonMember, challenge, challenge2;
before(async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
});
it('should return group challenges for non member with populated leader', async () => {
let challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`);
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
});
it('should return group challenges for member with populated leader', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
});
});
context('Private Guild', () => {
let privateGuild, user, nonMember, challenge, challenge2;
before(async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
privateGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
});
it('should prevent non-member from seeing challenges', async () => {
await expect(nonMember.get(`/challenges/groups/${privateGuild._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('should return group challenges for member with populated leader', async () => {
let challenges = await user.get(`/challenges/groups/${privateGuild._id}`);
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: privateGuild.leader._id,
id: privateGuild.leader._id,
profile: {name: user.profile.name},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: privateGuild.leader._id,
id: privateGuild.leader._id,
profile: {name: user.profile.name},
});
});
});
});

View File

@@ -0,0 +1,132 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET challenges/user', () => {
let user, member, nonMember, challenge, challenge2, publicGuild;
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
user = groupLeader;
publicGuild = group;
member = members[0];
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
});
it('should return challenges user has joined', async () => {
await nonMember.post(`/challenges/${challenge._id}/join`);
let challenges = await nonMember.get('/challenges/user');
let foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
expect(foundChallenge.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
expect(foundChallenge.group).to.eql({
_id: publicGuild._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
});
});
it('should return challenges user has created', async () => {
let challenges = await user.get('/challenges/user');
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
expect(foundChallenge1.group).to.eql({
_id: publicGuild._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
expect(foundChallenge2.group).to.eql({
_id: publicGuild._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
});
});
it('should return challenges in user\'s group', async () => {
let challenges = await member.get('/challenges/user');
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
expect(foundChallenge1.group).to.eql({
_id: publicGuild._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
});
expect(foundChallenge2.group).to.eql({
_id: publicGuild._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
});
});
it('should not return challenges user doesn\'t have access to', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
let privateChallenge = await generateChallenge(groupLeader, group);
let challenges = await nonMember.get('/challenges/user');
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
});

View File

@@ -0,0 +1,308 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges', () => {
it('returns error when group is empty', async () => {
let user = await generateUser();
await expect(user.post('/challenges')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when groupId is not for a valid group', async () => {
let user = await generateUser();
await expect(user.post('/challenges', {
group: generateUUID(),
})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns error when creating a challenge in the tavern with no prize', async () => {
let user = await generateUser();
await expect(user.post('/challenges', {
group: 'habitrpg',
prize: 0,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('tavChalsMinPrize'),
});
});
it('returns error when creating a challenge in a public guild and you are not a member of it', async () => {
let user = await generateUser();
let { group } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
});
await expect(user.post('/challenges', {
group: group._id,
prize: 4,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustBeGroupMember'),
});
});
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
let groupMember;
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
leaderDetails: {
balance: 3,
},
groupDetails: {
type: 'guild',
leaderOnly: {
challenges: true,
},
},
});
groupLeader = await populatedGroup.groupLeader.sync();
group = populatedGroup.group;
groupMember = populatedGroup.members[0];
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
});
group = populatedGroup.group;
groupMember = populatedGroup.members[0];
let chal = await groupMember.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
expect(chal.leader).to.eql({
_id: groupMember._id,
profile: {name: groupMember.profile.name},
});
});
it('doesn\'t take gems from user or group when challenge has no prize', async () => {
let oldUserBalance = groupLeader.balance;
let oldGroupBalance = group.balance;
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
prize: 0,
});
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldUserBalance);
await expect(group.sync()).to.eventually.have.property('balance', oldGroupBalance);
});
it('returns error when user and group can\'t pay prize', async () => {
await expect(groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
prize: 20,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantAfford'),
});
});
it('takes prize out of group if it has sufficient funds', async () => {
let oldUserBalance = groupLeader.balance;
let oldGroupBalance = group.balance;
let prize = 4;
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
prize,
});
await expect(group.sync()).to.eventually.have.property('balance', oldGroupBalance - prize / 4);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldUserBalance);
});
it('takes prize out of both group and user if group doesn\'t have enough', async () => {
let oldUserBalance = groupLeader.balance;
let prize = 8;
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
prize,
});
await expect(group.sync()).to.eventually.have.property('balance', 0);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldUserBalance - (prize / 4 - 1));
});
it('takes prize out of user if group has no balance', async () => {
let oldUserBalance = groupLeader.balance;
let prize = 8;
await group.update({ balance: 0});
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
prize,
});
await expect(group.sync()).to.eventually.have.property('balance', 0);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldUserBalance - prize / 4);
});
it('increases challenge count of group', async () => {
let oldChallengeCount = group.challengeCount;
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
await expect(group.sync()).to.eventually.have.property('challengeCount', oldChallengeCount + 1);
});
it('sets challenge as official if created by admin and official flag is set', async () => {
await groupLeader.update({
contributor: {
admin: true,
},
});
let challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
official: true,
});
expect(challenge.official).to.eql(true);
});
it('doesn\'t set challenge as official if official flag is set by non-admin', async () => {
let challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
official: true,
});
expect(challenge.official).to.eql(false);
});
it('returns an error when challenge validation fails; doesn\'s save user or group', async () => {
let oldChallengeCount = group.challengeCount;
let oldUserBalance = groupLeader.balance;
let oldUserChallenges = groupLeader.challenges;
let oldGroupBalance = group.balance;
await expect(groupLeader.post('/challenges', {
group: group._id,
prize: 8,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Challenge validation failed',
});
group = await group.sync();
groupLeader = await groupLeader.sync();
expect(group.challengeCount).to.eql(oldChallengeCount);
expect(group.balance).to.eql(oldGroupBalance);
expect(groupLeader.balance).to.eql(oldUserBalance);
expect(groupLeader.challenges).to.eql(oldUserChallenges);
});
it('sets all properites of the challenge as passed', async () => {
let name = 'Test Challenge';
let shortName = 'TC Label';
let description = 'Test Description';
let prize = 4;
let challenge = await groupLeader.post('/challenges', {
group: group._id,
name,
shortName,
description,
prize,
});
expect(challenge.leader).to.eql({
_id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
expect(challenge.name).to.eql(name);
expect(challenge.shortName).to.eql(shortName);
expect(challenge.description).to.eql(description);
expect(challenge.official).to.eql(false);
expect(challenge.group).to.eql({
_id: group._id,
privacy: group.privacy,
name: group.name,
type: group.type,
});
expect(challenge.memberCount).to.eql(1);
expect(challenge.prize).to.eql(prize);
});
it('adds challenge to creator\'s challenges', async () => {
let challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
});
});
});

View File

@@ -0,0 +1,127 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/join', () => {
it('returns error when challengeId is not a valid UUID', async () => {
let user = await generateUser({ balance: 1});
await expect(user.post('/challenges/test/join')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when challengeId is not for a valid challenge', async () => {
let user = await generateUser({ balance: 1});
await expect(user.post(`/challenges/${generateUUID()}/join`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
context('Joining a valid challenge', () => {
let groupLeader;
let group;
let challenge;
let authorizedUser;
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
authorizedUser = populatedGroup.members[0];
challenge = await generateChallenge(groupLeader, group);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('returns challenge data', async () => {
let res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
expect(res.group).to.eql({
_id: group._id,
privacy: group.privacy,
name: group.name,
type: group.type,
});
expect(res.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
expect(res.name).to.equal(challenge.name);
});
it('adds challenge to user challenges', async () => {
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await authorizedUser.sync();
expect(authorizedUser).to.have.property('challenges').to.include(challenge._id);
});
it('returns error when user has already joined the challenge', async () => {
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await expect(authorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInChallenge'),
});
});
it('increases memberCount of challenge', async () => {
let oldMemberCount = challenge.memberCount;
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge).to.have.property('memberCount', oldMemberCount + 1);
});
it('syncs challenge tasks to joining user', async () => {
let taskText = 'A challenge task text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
]);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
let tasks = await authorizedUser.get('/tasks/user');
let tasksTexts = tasks.map((task) => {
return task.text;
});
expect(tasksTexts).to.include(taskText);
});
it('adds challenge tag to user tags', async () => {
let userTagsLength = (await authorizedUser.get('/tags')).length;
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
});
});
});

View File

@@ -0,0 +1,123 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/leave', () => {
it('returns error when challengeId is not a valid UUID', async () => {
let user = await generateUser();
await expect(user.post('/challenges/test/leave')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when challengeId is not for a valid challenge', async () => {
let user = await generateUser();
await expect(user.post(`/challenges/${generateUUID()}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
context('Leaving a valid challenge', () => {
let groupLeader;
let group;
let challenge;
let notInChallengeUser;
let leavingUser;
let taskText;
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
leavingUser = populatedGroup.members[0];
notInChallengeUser = populatedGroup.members[1];
challenge = await generateChallenge(groupLeader, group);
taskText = 'A challenge task text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
]);
await leavingUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
});
it('returns an error when user doesn\'t have permissions to view the challenge', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('returns an error when user isn\'t a member of the challenge', async () => {
await expect(notInChallengeUser.post(`/challenges/${challenge._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('challengeMemberNotFound'),
});
});
it('removes challenge from user challenges', async () => {
await leavingUser.post(`/challenges/${challenge._id}/leave`);
await leavingUser.sync();
expect(leavingUser).to.have.property('challenges').to.not.include(challenge._id);
});
it('decreases memberCount of challenge', async () => {
let oldMemberCount = challenge.memberCount;
await leavingUser.post(`/challenges/${challenge._id}/leave`);
await challenge.sync();
expect(challenge).to.have.property('memberCount', oldMemberCount - 1);
});
it('unlinks challenge tasks from leaving user when remove-all is passed', async () => {
await leavingUser.post(`/challenges/${challenge._id}/leave`, {
keep: 'remove-all',
});
let tasks = await leavingUser.get('/tasks/user');
let tasksTexts = tasks.map((task) => {
return task.text;
});
expect(tasksTexts).to.not.include(taskText);
});
it('doesn\'t unlink challenge tasks from leaving user when remove-all isn\'t passed', async () => {
await leavingUser.post(`/challenges/${challenge._id}/leave`, {
keep: 'test',
});
let tasks = await leavingUser.get('/tasks/user');
let testTask = _.find(tasks, (task) => {
return task.text === taskText;
});
expect(testTask).to.not.be.undefined;
expect(testTask.challenge).to.be.undefined;
});
});
});

View File

@@ -0,0 +1,139 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
sleep,
checkExistence,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/winner/:winnerId', () => {
it('returns error when challengeId is not a valid UUID', async () => {
let user = await generateUser();
await expect(user.post(`/challenges/test/selectWinner/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when winnerId is not a valid UUID', async () => {
let user = await generateUser();
await expect(user.post(`/challenges/${generateUUID()}/selectWinner/test`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when challengeId is not for a valid challenge', async () => {
let user = await generateUser();
await expect(user.post(`/challenges/${generateUUID()}/selectWinner/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
context('Selecting winner for a valid challenge', () => {
let groupLeader;
let group;
let challenge;
let winningUser;
let taskText = 'A challenge task text';
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
winningUser = populatedGroup.members[0];
challenge = await generateChallenge(groupLeader, group, {
prize: 1,
});
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
]);
await winningUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
});
it('returns an error when user doesn\'t have permissions to select winner', async () => {
await expect(winningUser.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyLeaderDeleteChal'),
});
});
it('returns an error when winning user isn\'t part of the challenge', async () => {
let notInChallengeUser = await generateUser();
await expect(groupLeader.post(`/challenges/${challenge._id}/selectWinner/${notInChallengeUser._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('winnerNotFound', {userId: notInChallengeUser._id}),
});
});
it('deletes challenge after winner is selected', async () => {
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(checkExistence('challenges', challenge._id)).to.eventually.equal(false);
});
it('adds challenge to winner\'s achievements', async () => {
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
});
it('gives winner gems as reward', async () => {
let oldBalance = winningUser.balance;
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4);
});
it('doesn\'t refund gems to group leader', async () => {
let oldBalance = (await groupLeader.sync()).balance;
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldBalance);
});
it('sets broken and winner flags for user\'s challenge tasks', async () => {
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
let tasks = await winningUser.get('/tasks/user');
let testTask = _.find(tasks, (task) => {
return task.text === taskText;
});
expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED');
expect(testTask.challenge.winner).to.eql(winningUser.profile.name);
});
});
});

View File

@@ -0,0 +1,85 @@
import {
generateUser,
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('PUT /challenges/:challengeId', () => {
let privateGuild, user, nonMember, challenge, member;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
privateGuild = group;
user = groupLeader;
nonMember = await generateUser();
member = members[0];
challenge = await generateChallenge(user, group);
await member.post(`/challenges/${challenge._id}/join`);
});
it('fails if the user can\'t view the challenge', async () => {
await expect(nonMember.put(`/challenges/${challenge._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should only allow the leader or an admin to update the challenge', async () => {
await expect(member.put(`/challenges/${challenge._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyLeaderUpdateChal'),
});
});
it('only updates allowed fields', async () => {
let res = await user.put(`/challenges/${challenge._id}`, {
// ignored
prize: 33,
group: 'blabla',
memberCount: 33,
tasksOrder: 'new order',
official: true,
shortName: 'new short name',
// applied
name: 'New Challenge Name',
description: 'New challenge description.',
leader: member._id,
});
expect(res.prize).to.equal(0);
expect(res.group).to.eql({
_id: privateGuild._id,
privacy: privateGuild.privacy,
name: privateGuild.name,
type: privateGuild.type,
});
expect(res.memberCount).to.equal(2);
expect(res.tasksOrder).not.to.equal('new order');
expect(res.official).to.equal(false);
expect(res.shortName).not.to.equal('new short name');
expect(res.leader).to.eql({
_id: member._id,
id: member._id,
profile: {name: member.profile.name},
});
expect(res.name).to.equal('New Challenge Name');
expect(res.description).to.equal('New challenge description.');
});
});