mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-10 19:20:21 -05:00
Compare commits
349 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4951c6478 | |||
| 95285cd85a | |||
| bf8b2db6b3 | |||
| 116ec0f9d9 | |||
| a89633d17b | |||
| 2e3dd27414 | |||
| 3af756a90d | |||
| e4d006e5cd | |||
| 801b53857f | |||
| f8571ec5d5 | |||
| 78ba596504 | |||
| fe9521a63f | |||
| c4348d8e47 | |||
| 9b0ee6a726 | |||
| c23fad3077 | |||
| 6850a1fcb4 | |||
| bc477455bb | |||
| 88c56c9877 | |||
| 4cd272f99c | |||
| 0292501387 | |||
| 28b56256d2 | |||
| a4ee3aaa7e | |||
| 7a2432a1a0 | |||
| 0bf515d0f9 | |||
| cbed198f02 | |||
| 90d77cee81 | |||
| 3668a9d3ac | |||
| 052c653cd3 | |||
| 3aa7b4b631 | |||
| 045378b820 | |||
| a256b6df03 | |||
| 92fdc13adf | |||
| dd29c60d87 | |||
| 78fd79931e | |||
| 5055a57ae9 | |||
| 3fa0b72ffe | |||
| b3f9fd09c6 | |||
| 30437e158e | |||
| 19ba1290f6 | |||
| c509c8e04f | |||
| 00d4393024 | |||
| c502b1997b | |||
| 4435862ff2 | |||
| e901850a6f | |||
| 48bbc22fb4 | |||
| c1e5d8b573 | |||
| 6951b79b95 | |||
| 1c3a12f37d | |||
| 3c71748a1b | |||
| 8e56222fc7 | |||
| ac3ac73737 | |||
| 8004c1e759 | |||
| d9e50e7632 | |||
| 4cd742c19a | |||
| 6bc6c09c75 | |||
| 0cbf9354cc | |||
| 03ca65f2b8 | |||
| ea74271484 | |||
| 0897367538 | |||
| 2566fcecb7 | |||
| c99863f339 | |||
| 71d632f6f0 | |||
| 06c53677f6 | |||
| aa91c5dbae | |||
| 5b7c7b77c8 | |||
| d3d221dccb | |||
| 53c610236b | |||
| 548cc2ceb0 | |||
| bf03ac8e44 | |||
| 1853113aed | |||
| 1ce1ed7c6b | |||
| c9f870a929 | |||
| 135df1dc48 | |||
| a240d9581f | |||
| 3cd591ed4f | |||
| 87076ed07c | |||
| 33a39d3683 | |||
| e4f5950ffc | |||
| f2d81a8d9c | |||
| 43b6f71044 | |||
| 06de1670b4 | |||
| 7fd2522e93 | |||
| acb4b79078 | |||
| 8299982484 | |||
| 207b1e91ca | |||
| aee21edd5f | |||
| 6e1bbd05bc | |||
| 763c2c28ea | |||
| 7aa9ba45f6 | |||
| d67398fba6 | |||
| 6fb4c1b576 | |||
| 7c5bd526b1 | |||
| 187a9f5f19 | |||
| 7954763738 | |||
| d062fd8507 | |||
| ceb10137a2 | |||
| 58330a4d01 | |||
| 678f48fde9 | |||
| a642d94443 | |||
| 39a112b605 | |||
| 8c8f0ea201 | |||
| ca8541e8c4 | |||
| 773d546e4f | |||
| f269d381da | |||
| 3483f69559 | |||
| 9ada0c9b02 | |||
| 8bbe9ac36e | |||
| 75b93e6ec4 | |||
| bd9bebe591 | |||
| f7b298d506 | |||
| 4bd2932955 | |||
| 7060f5941d | |||
| 21379ee357 | |||
| fe62713809 | |||
| ca7272a987 | |||
| cb46cd8eeb | |||
| d9d02ca81d | |||
| 50c216eb41 | |||
| 2c60fade01 | |||
| 6bdf8fdabc | |||
| 3db304b6bf | |||
| ea2788ab2f | |||
| 5e111f6f6c | |||
| ea85a8a3a8 | |||
| e811a2700e | |||
| 136dcd27a9 | |||
| bf76f823d5 | |||
| 83b573dcfc | |||
| c4fa9426b3 | |||
| 042e5a8d63 | |||
| 7a8857010e | |||
| a52bd66871 | |||
| f7ce269f3c | |||
| c084f8a2b9 | |||
| c1c42e17b8 | |||
| 306a782e7a | |||
| 20178c0722 | |||
| 256e2e809c | |||
| 592345e22c | |||
| f738f550e7 | |||
| 495bc9aa50 | |||
| 292b2acb1e | |||
| 977f9d5174 | |||
| 36fa3ab06f | |||
| 7422d020b1 | |||
| 5d0fe0aac3 | |||
| 85644fdc1b | |||
| 138b5c4bdb | |||
| 52edb8a8da | |||
| 60de7c8f21 | |||
| 137636cb40 | |||
| 1999e1098e | |||
| 4d3a0c0571 | |||
| 706de95458 | |||
| e3c1eaa9d2 | |||
| 17c0f795cc | |||
| cb5ac9014e | |||
| 0be681b7a2 | |||
| 5360f9e587 | |||
| 4553a411f6 | |||
| 613f51b08d | |||
| 2292ba2694 | |||
| befacca457 | |||
| 5cd30b430d | |||
| c5d9ee1e0a | |||
| 234328f2ba | |||
| 029afa197e | |||
| 05b35c5147 | |||
| c9427ad34c | |||
| d6c62262f1 | |||
| ec1d378504 | |||
| 3bb88f450a | |||
| 97a38e68c5 | |||
| be948a1bf2 | |||
| 018976a723 | |||
| 00e5896ac6 | |||
| 36bc693545 | |||
| f27706cb4b | |||
| f6f99ec57e | |||
| c852d9d581 | |||
| 4a78514308 | |||
| 265b48752d | |||
| db0b0d6b6d | |||
| 20f1087552 | |||
| 2e9bc2c31c | |||
| b606dd1c40 | |||
| db1c2fd5a2 | |||
| de1e477ce2 | |||
| 67318177a2 | |||
| cd1be828ca | |||
| 9ffebc10a7 | |||
| 5cd11ed343 | |||
| 9de118f0d9 | |||
| 5fbec4069e | |||
| a0f10cbf4b | |||
| 0e069e78d5 | |||
| dea847ba1a | |||
| 46ed1813c6 | |||
| e9750353a7 | |||
| 05ea2c1ce6 | |||
| 74d1c7763e | |||
| 6f034bb5dd | |||
| aeb8d4f500 | |||
| 58d910fe62 | |||
| 71f2f31606 | |||
| feae40cf0a | |||
| 0d3fe53155 | |||
| c3a3c1514a | |||
| cc532fa993 | |||
| ba66a1c098 | |||
| b4d5c634b3 | |||
| 216006beab | |||
| 7c236e7e0e | |||
| 0221d2d7f9 | |||
| 567eb1d98b | |||
| 3cf533f261 | |||
| c30c51f386 | |||
| 2de794c32b | |||
| 7d000d2cf6 | |||
| 280b720c13 | |||
| 9e1f7f3811 | |||
| fefaed368a | |||
| 6f370395ea | |||
| 65566f7607 | |||
| c0117706e4 | |||
| f267456a30 | |||
| 228b724d52 | |||
| 19b09b4894 | |||
| f49d21d7b4 | |||
| c08c0685f3 | |||
| 59bfe66c94 | |||
| 023fd6e6b0 | |||
| 7ee2f90f37 | |||
| 46709ddadd | |||
| a3ee09e764 | |||
| 4127f36c02 | |||
| dad924ac7d | |||
| 547c87dee7 | |||
| 99a2013767 | |||
| a5e0e171cc | |||
| 1d93943458 | |||
| 7f2719a75c | |||
| 8a9ed04f5e | |||
| 4e66c038a6 | |||
| bd9618e1c6 | |||
| 342034a80d | |||
| f2aed3cb27 | |||
| 4f5f15d162 | |||
| b5c1b78789 | |||
| f2ace5bb63 | |||
| 72b8ba2d5c | |||
| 8555e4bea8 | |||
| 4de5b4253b | |||
| 410004f8c6 | |||
| 76c7e15497 | |||
| e6f605f23a | |||
| 0af1203832 | |||
| 1de379a2c3 | |||
| 388861b503 | |||
| f52806ed69 | |||
| d89b9e08af | |||
| f8a99bd127 | |||
| a82b60f144 | |||
| 65e71140ee | |||
| f192ca4c6f | |||
| 36b09d40b9 | |||
| 4fdd754b31 | |||
| 36ff063991 | |||
| 20dcb1cb8a | |||
| 3a1527073f | |||
| 1292f9a3d5 | |||
| e7418472f6 | |||
| 850f332ddc | |||
| 59fb32ea2e | |||
| 727cdc9402 | |||
| 638c9dee89 | |||
| 168ed02226 | |||
| aa253cf72e | |||
| 247742c60d | |||
| 7d89deb094 | |||
| c128b701fa | |||
| 2adac35e31 | |||
| f01e13ffc0 | |||
| 4e9d631b71 | |||
| f1de7c02e9 | |||
| 15d4f7d6ab | |||
| 76222ac344 | |||
| 2659a4117b | |||
| a0ee73e944 | |||
| 6174624b89 | |||
| c8b6e8ea7c | |||
| 409b5d5965 | |||
| e7209511ca | |||
| 8c76ccd39b | |||
| 399c91ccab | |||
| 4bbebdd237 | |||
| 4f305bd505 | |||
| 024feaa2f4 | |||
| d2dc8f1856 | |||
| e3a08c1905 | |||
| dad0eea9e0 | |||
| 864ca91144 | |||
| 78816dd4cb | |||
| 6a99daebac | |||
| c9ee6c7f73 | |||
| e2f4b0e3dc | |||
| 369702884a | |||
| d438990d18 | |||
| 7d3213fd66 | |||
| bde4eafc05 | |||
| 5a2ba27808 | |||
| 024268a51e | |||
| f7281e71e8 | |||
| 91438aff90 | |||
| 91ed55cf66 | |||
| 4275da0a2e | |||
| d9f48dcbb0 | |||
| bd6f901ccf | |||
| 884bf02961 | |||
| 7d42e8fc71 | |||
| 0442b87608 | |||
| c1d1a3e14e | |||
| dd5a9aa6cc | |||
| 0bb6e5f3fc | |||
| 69347df050 | |||
| 9cad5525e6 | |||
| b1e6aceffe | |||
| 17068875f4 | |||
| 342fc2e344 | |||
| b8b1557e49 | |||
| f90ef04e83 | |||
| d0561512de | |||
| b84c672f33 | |||
| aafbb889be | |||
| 13865bcf49 | |||
| f9ca69196a | |||
| 0e77df6e7b | |||
| f2d9bdf7ae | |||
| 2d7e280598 | |||
| d1de41290d | |||
| 0fd85c0d60 | |||
| 6956f5345e | |||
| cc68abb67e | |||
| b48be4e619 | |||
| 7a543d07a4 | |||
| 9115be68b2 | |||
| 8c68f450c6 | |||
| 83bd4dcf60 | |||
| 92b02295b5 |
@@ -1,6 +1,6 @@
|
||||
# Reporting Bugs
|
||||
|
||||
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitrpg/issues/2760)
|
||||
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
# Pull Request
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
|
||||
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
||||
### General Info
|
||||
|
||||
@@ -2,6 +2,8 @@ language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
sudo: required
|
||||
services:
|
||||
- mongodb
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
@@ -12,6 +14,8 @@ before_install:
|
||||
- $CXX --version
|
||||
- npm install -g npm@4
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
install:
|
||||
- npm install &> npm.install.log || (cat npm.install.log; false)
|
||||
before_script:
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
@@ -31,3 +35,4 @@ env:
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="test:karma" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
- TEST="apidoc"
|
||||
|
||||
+18
-45
@@ -1,45 +1,18 @@
|
||||
FROM ubuntu:trusty
|
||||
|
||||
MAINTAINER Sabe Jones <sabe@habitica.com>
|
||||
|
||||
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
|
||||
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
|
||||
|
||||
# Install prerequisites
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
git \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
libkrb5-dev \
|
||||
python
|
||||
|
||||
# Install NodeJS
|
||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Install npm@latest
|
||||
RUN curl -sL https://www.npmjs.org/install.sh | sh
|
||||
|
||||
# Clean up package management
|
||||
RUN apt-get clean
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp grunt-cli bower mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
WORKDIR /habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
|
||||
RUN npm install
|
||||
RUN bower install --allow-root
|
||||
|
||||
# Create environment config file and build directory
|
||||
RUN cp config.json.example config.json
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
FROM node:boron
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp grunt-cli bower mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN bower install --allow-root
|
||||
|
||||
# Create Build dir
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:boron
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp grunt-cli bower mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone --branch v3.102.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN bower install --allow-root
|
||||
RUN gulp build:prod --force
|
||||
|
||||
# Create Build dir
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
EXPOSE 3000
|
||||
CMD ["node", "./website/transpiled-babel/index.js"]
|
||||
@@ -5,8 +5,7 @@ Habitica [ - "Coders (Web & Mobile)" section.
|
||||
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
|
||||
|
||||
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
|
||||
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
|
||||
|
||||
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
|
||||
|
||||
+6
-1
@@ -66,7 +66,8 @@
|
||||
},
|
||||
"mode":"sandbox",
|
||||
"client_id":"client_id",
|
||||
"client_secret":"client_secret"
|
||||
"client_secret":"client_secret",
|
||||
"experience_profile_id": ""
|
||||
},
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"LOGGLY_TOKEN": "token",
|
||||
@@ -97,5 +98,9 @@
|
||||
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
|
||||
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
|
||||
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
|
||||
},
|
||||
"LOGGLY" : {
|
||||
"TOKEN" : "example-token",
|
||||
"SUBDOMAIN" : "exmaple-subdomain"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
|
||||
|
||||
/**
|
||||
* database_reports/count_users_who_own_specified_gear.js
|
||||
* https://github.com/HabitRPG/habitrpg/pull/3884
|
||||
* https://github.com/HabitRPG/habitica/pull/3884
|
||||
*/
|
||||
|
||||
var thingsOfInterest = {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// Copy Bootstrap 4 config variables from /website /node_modules so we can check
|
||||
// them into Git
|
||||
|
||||
const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.scss';
|
||||
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
|
||||
|
||||
// https://stackoverflow.com/a/14387791/969528
|
||||
function copyFile(source, target, cb) {
|
||||
let cbCalled = false;
|
||||
|
||||
function done(err) {
|
||||
if (!cbCalled) {
|
||||
cb(err);
|
||||
cbCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
let rd = fs.createReadStream(source);
|
||||
rd.on('error', done);
|
||||
let wr = fs.createWriteStream(target);
|
||||
wr.on('error', done);
|
||||
wr.on('close', () => done());
|
||||
rd.pipe(wr);
|
||||
}
|
||||
|
||||
gulp.task('bootstrap', (done) => {
|
||||
// use new config
|
||||
copyFile(
|
||||
BOOSTRAP_NEW_CONFIG_PATH,
|
||||
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
|
||||
done,
|
||||
);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
|
||||
});
|
||||
|
||||
if (numberOfSheetsThatAreTooBig > 0) {
|
||||
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitrpg/pull/6683#issuecomment-185462180
|
||||
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ if (process.env.NODE_ENV === 'production') {
|
||||
require('./gulp/gulp-newstuff');
|
||||
require('./gulp/gulp-build');
|
||||
require('./gulp/gulp-babelify');
|
||||
require('./gulp/gulp-bootstrap');
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require);
|
||||
require('gulp').task('default', ['test']);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Habitica in Kubernetes
|
||||
This is a set of sample Kubernetes configuration files to launch Habitica under AWS, both as a single-node web frontend as well as a multi-node web frontend.
|
||||
|
||||
## Prerequisites
|
||||
* An AWS account.
|
||||
* A working Kubernetes installation.
|
||||
* A basic understanding of how to use Kubernetes. https://kubernetes.io/
|
||||
* A persistent volume for MongoDB data.
|
||||
* Docker images of Habitica.
|
||||
+ You can use your own, or use the one included in the YAML files.
|
||||
+ If you use your own, you'll need a fork of the Habitica GitHub repo and your own Docker Hub repo, both of which are free.
|
||||
|
||||
## Before you begin
|
||||
1. Set up Kubernetes.
|
||||
2. Create an EBS volume for MongoDB data.
|
||||
+ Make a note of the name, you'll need it later.
|
||||
|
||||
## Starting MongoDB
|
||||
1. Edit mongo.yaml
|
||||
+ Find the volumeID line.
|
||||
+ Change the volume to the one created in the section above.
|
||||
2. Run the following commands:
|
||||
+ `kubectl.sh create -f mongo.yaml`
|
||||
+ `kubectl.sh create -f mongo-service.yaml`
|
||||
3. Wait for the MongoDB pod to start up.
|
||||
|
||||
## Starting a Single Web Frontend
|
||||
|
||||
1. Run the following commands:
|
||||
+ `kubectl.sh create -f habitica.yaml`
|
||||
+ `kubectl.sh create -f habitica-service.yaml`
|
||||
2. Wait for the frontend to start up.
|
||||
|
||||
## Starting Multi-node Web Frontend
|
||||
1. Run the following commands :
|
||||
+ `kubectl.sh create -f habitica-rc.yaml`
|
||||
+ `kubectl.sh create -f habitica-service.yaml`
|
||||
2. Wait for the frontend to start up.
|
||||
|
||||
## Accessing Your Habitica web interface
|
||||
Using `kubectl describe svc habiticaweb` get the hostname generated for the Habitica service. Open a browser and go to http://hostname:3000 to access the web front-end for the installations above.
|
||||
|
||||
## Shutting down
|
||||
Shutting down is basically done by reversing the steps above:
|
||||
+ `kubectl.sh delete -f habitica-service.yaml`
|
||||
+ `kubectl.sh delete -f habitica.yaml (or habitica-rc.yaml)`
|
||||
+ `kubectl.sh delete -f mongo-service.yaml`
|
||||
+ `kubectl.sh delete -f mongo.yaml`
|
||||
|
||||
You can also just shut down all of Kubernetes as well.
|
||||
|
||||
## Notes
|
||||
+ MongoDB data will be persistent! If you need to start with a fresh database, you'll need to remove the volume and re-create it.
|
||||
+ On AWS, you probably want to use at least t2.medium minion nodes for Kubernetes. The default t2.small is too small for more than two Habitica nodes.
|
||||
|
||||
## Future Plans
|
||||
+ Multi-node MongoDB.
|
||||
+ Monitoring
|
||||
+ Instructions for a better hostname. The default generated ones stink.
|
||||
+ More to come....
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: habitica
|
||||
labels:
|
||||
name: habitica
|
||||
spec:
|
||||
replicas: 4
|
||||
selector:
|
||||
name: habitica
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: habitica
|
||||
spec:
|
||||
containers:
|
||||
- name: habitica
|
||||
image: ksonney/habitrpg:latest
|
||||
env:
|
||||
- name: NODE_DB_URI
|
||||
value: mongodb://mongosvc/habitrpg
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
hostPort: 3000
|
||||
name: habitica
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
name: habiticaweb
|
||||
name: habiticaweb
|
||||
spec:
|
||||
ports:
|
||||
# the port that this service should serve on
|
||||
- port: 3000
|
||||
# label keys and values that must match in order to receive traffic for this service
|
||||
selector:
|
||||
name: habitica
|
||||
type: LoadBalancer
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: habitica
|
||||
labels:
|
||||
name: habitica
|
||||
spec:
|
||||
containers:
|
||||
# - image: mongo:latest
|
||||
# name: mongo
|
||||
# ports:
|
||||
# - containerPort: 27017
|
||||
# name: mongo
|
||||
- image: ksonney/habitrpg:latest
|
||||
name: habitica
|
||||
env:
|
||||
- name: NODE_DB_URI
|
||||
value: mongodb://mongosvc/habitrpg
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
hostPort: 3000
|
||||
name: habitica
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
name: mongosvc
|
||||
name: mongosvc
|
||||
spec:
|
||||
ports:
|
||||
# the port that this service should serve on
|
||||
- port: 27017
|
||||
# label keys and values that must match in order to receive traffic for this service
|
||||
selector:
|
||||
name: mongodb
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: mongodb
|
||||
labels:
|
||||
name: mongodb
|
||||
spec:
|
||||
containers:
|
||||
- resources:
|
||||
limits :
|
||||
cpu: 0.5
|
||||
image: mongo
|
||||
name: mongodb
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
hostPort: 27017
|
||||
name: mongo
|
||||
volumeMounts:
|
||||
# # name must match the volume name below
|
||||
- name: mongo-persistent-storage
|
||||
# # mount path within the container
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongo-persistent-storage
|
||||
awsElasticBlockStore:
|
||||
volumeID: aws://YOUR-REGION/YOUR-VOLNAME
|
||||
fsType: ext3
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
/****************************************
|
||||
* Author: @Alys
|
||||
*
|
||||
* Reason: Collection quests are being changed
|
||||
* to require fewer items collected:
|
||||
* https://github.com/HabitRPG/habitrpg/pull/7987
|
||||
* This will cause existing quests to end sooner
|
||||
* than the party is expecting.
|
||||
* This script inserts an explanatory `system`
|
||||
* message into the chat for affected parties.
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const uuid = require('uuid');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
const connectToDb = require('./utils/connect').connectToDb;
|
||||
const closeDb = require('./utils/connect').closeDb;
|
||||
|
||||
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.wikia.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
|
||||
|
||||
const timer = new Timer();
|
||||
|
||||
// PROD: Enable prod db
|
||||
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
|
||||
const DB_URI = 'mongodb://localhost/habitrpg';
|
||||
|
||||
const COLLECTION_QUESTS = [
|
||||
'vice2',
|
||||
'egg',
|
||||
'moonstone1',
|
||||
'goldenknight1',
|
||||
'dilatoryDistress1',
|
||||
];
|
||||
|
||||
let Groups;
|
||||
|
||||
connectToDb(DB_URI).then((db) => {
|
||||
Groups = db.collection('groups');
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(findPartiesWithCollectionQuest)
|
||||
// .then(displayGroups) // for testing only
|
||||
.then(addMessageToGroups)
|
||||
.then(() => {
|
||||
timer.stop();
|
||||
closeDb();
|
||||
}).catch(reportError);
|
||||
|
||||
function reportError (err) {
|
||||
logger.error('Uh oh, an error occurred');
|
||||
closeDb();
|
||||
timer.stop();
|
||||
throw err;
|
||||
}
|
||||
|
||||
function findPartiesWithCollectionQuest () {
|
||||
logger.info('Looking up groups on collection quests...');
|
||||
|
||||
return Groups.find({'quest.key': {$in: COLLECTION_QUESTS}}, ['name','quest']).toArray().then((groups) => {
|
||||
logger.success('Found', groups.length, 'parties on collection quests');
|
||||
|
||||
return Promise.resolve(groups);
|
||||
})
|
||||
}
|
||||
|
||||
function displayGroups (groups) { // for testing only
|
||||
logger.info('Displaying parties...');
|
||||
console.log(groups);
|
||||
return Promise.resolve(groups);
|
||||
}
|
||||
|
||||
function updateGroupById (group) {
|
||||
var newMessage = {
|
||||
'id' : uuid.v4(),
|
||||
'text' : message,
|
||||
'timestamp': Date.now(),
|
||||
'likes': {},
|
||||
'flags': {},
|
||||
'flagCount': 0,
|
||||
'uuid': 'system'
|
||||
};
|
||||
return Groups.findOneAndUpdate({_id: group._id}, {$push:{"chat" :{$each: [newMessage], $position:0}}}, {returnOriginal: false});
|
||||
// Does not set the newMessage flag for all party members because I don't think it's essential and
|
||||
// I don't want to run the extra code (extra database load, extra opportunity for bugs).
|
||||
}
|
||||
|
||||
function addMessageToGroups (groups) {
|
||||
let queue = new TaskQueue(Promise, 300);
|
||||
|
||||
logger.info('About to update', groups.length, 'parties...');
|
||||
|
||||
return Promise.map(groups, queue.wrap(updateGroupById)).then((result) => {
|
||||
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
|
||||
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
|
||||
|
||||
logger.success(updates.length, 'parties have been notified');
|
||||
|
||||
if (failures.length > 0) {
|
||||
logger.error(failures.length, 'parties could not be notified');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
var migrationName = '20170616_achievements';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Updates to achievements for June 16, 2017 biweekly merge
|
||||
* 1. Multiply various collection quest achievements based on difficulty reduction
|
||||
* 2. Award Joined Challenge achievement to those who should have it already
|
||||
*/
|
||||
|
||||
import monk from 'monk';
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
$or: [
|
||||
{'achievements.quests.dilatoryDistress1': {$gt:0}},
|
||||
{'achievements.quests.egg': {$gt:0}},
|
||||
{'achievements.quests.goldenknight1': {$gt:0}},
|
||||
{'achievements.quests.moonstone1': {$gt:0}},
|
||||
{'achievements.quests.vice2': {$gt:0}},
|
||||
{'achievements.challenges': {$exists: true, $ne: []}},
|
||||
{'challenges': {$exists: true, $ne: []}},
|
||||
],
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
'achievements',
|
||||
'challenges',
|
||||
],
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
var set = {'migration': migrationName};
|
||||
|
||||
if (user.challenges.length > 0 || user.achievements.challenges.length > 0) {
|
||||
set['achievements.joinedChallenge'] = true;
|
||||
}
|
||||
if (user.achievements.quests.dilatoryDistress1) {
|
||||
set['achievements.quests.dilatoryDistress1'] = Math.ceil(user.achievements.quests.dilatoryDistress1 * 1.25);
|
||||
}
|
||||
if (user.achievements.quests.egg) {
|
||||
set['achievements.quests.egg'] = Math.ceil(user.achievements.quests.egg * 2.5);
|
||||
}
|
||||
if (user.achievements.quests.goldenknight1) {
|
||||
set['achievements.quests.goldenknight1'] = user.achievements.quests.goldenknight1 * 5;
|
||||
}
|
||||
if (user.achievements.quests.moonstone1) {
|
||||
set['achievements.quests.moonstone1'] = user.achievements.quests.moonstone1 * 5;
|
||||
}
|
||||
if (user.achievements.quests.vice2) {
|
||||
set['achievements.quests.vice2'] = Math.ceil(user.achievements.quests.vice2 * 1.5);
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -0,0 +1,90 @@
|
||||
var migrationName = '20170711_orcas.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award Orca pets to owners of Orca mount, and Orca mount to everyone else
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.mounts',
|
||||
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {};
|
||||
|
||||
if (user.items.mounts['Orca-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.Orca-Base': 5};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.mounts.Orca-Base': true};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -1,47 +1,47 @@
|
||||
import Bluebird from 'Bluebird';
|
||||
|
||||
import { model as Challenges } from '../../website/server/models/challenge';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
async function syncChallengeToMembers (challenges) {
|
||||
let challengSyncPromises = challenges.map(async function (challenge) {
|
||||
let users = await User.find({
|
||||
// _id: '',
|
||||
challenges: challenge._id,
|
||||
}).exec();
|
||||
|
||||
let promises = [];
|
||||
users.forEach(function (user) {
|
||||
promises.push(challenge.syncToUser(user));
|
||||
promises.push(challenge.save());
|
||||
promises.push(user.save());
|
||||
});
|
||||
|
||||
return Bluebird.all(promises);
|
||||
});
|
||||
|
||||
return await Bluebird.all(challengSyncPromises);
|
||||
}
|
||||
|
||||
async function syncChallenges (lastChallengeDate) {
|
||||
let query = {
|
||||
// _id: '',
|
||||
};
|
||||
|
||||
if (lastChallengeDate) {
|
||||
query.createdOn = { $lte: lastChallengeDate };
|
||||
}
|
||||
|
||||
let challengesFound = await Challenges.find(query)
|
||||
.limit(10)
|
||||
.sort('-createdAt')
|
||||
.exec();
|
||||
|
||||
let syncedChallenges = await syncChallengeToMembers(challengesFound)
|
||||
.catch(reason => console.error(reason));
|
||||
let lastChallenge = challengesFound[challengesFound.length - 1];
|
||||
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
|
||||
return syncedChallenges;
|
||||
};
|
||||
|
||||
module.exports = syncChallenges;
|
||||
import Bluebird from 'Bluebird';
|
||||
|
||||
import { model as Challenges } from '../../website/server/models/challenge';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
async function syncChallengeToMembers (challenges) {
|
||||
let challengSyncPromises = challenges.map(async function (challenge) {
|
||||
let users = await User.find({
|
||||
// _id: '',
|
||||
challenges: challenge._id,
|
||||
}).exec();
|
||||
|
||||
let promises = [];
|
||||
users.forEach(function (user) {
|
||||
promises.push(challenge.syncToUser(user));
|
||||
promises.push(challenge.save());
|
||||
promises.push(user.save());
|
||||
});
|
||||
|
||||
return Bluebird.all(promises);
|
||||
});
|
||||
|
||||
return await Bluebird.all(challengSyncPromises);
|
||||
}
|
||||
|
||||
async function syncChallenges (lastChallengeDate) {
|
||||
let query = {
|
||||
// _id: '',
|
||||
};
|
||||
|
||||
if (lastChallengeDate) {
|
||||
query.createdOn = { $lte: lastChallengeDate };
|
||||
}
|
||||
|
||||
let challengesFound = await Challenges.find(query)
|
||||
.limit(10)
|
||||
.sort('-createdAt')
|
||||
.exec();
|
||||
|
||||
let syncedChallenges = await syncChallengeToMembers(challengesFound)
|
||||
.catch(reason => console.error(reason));
|
||||
let lastChallenge = challengesFound[challengesFound.length - 1];
|
||||
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
|
||||
return syncedChallenges;
|
||||
};
|
||||
|
||||
module.exports = syncChallenges;
|
||||
|
||||
@@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans');
|
||||
processUsers()
|
||||
.catch(function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['back_mystery_201704','armor_mystery_201704']
|
||||
$each:['body_mystery_201706','back_mystery_201706']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
var migrationName = 'tasks-set-yesterdaily';
|
||||
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
|
||||
var authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* Iterates over all tasks and sets the yseterDaily field to True
|
||||
*/
|
||||
|
||||
import monk from 'monk';
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbTasks = monk(connectionString).get('tasks', { castIds: false });
|
||||
|
||||
function processTasks(lastId) {
|
||||
// specify a query to limit the affected tasks (empty for all tasks):
|
||||
var query = {
|
||||
yesterDaily: false,
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbTasks.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
],
|
||||
})
|
||||
.then(updateTasks)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateTasks (tasks) {
|
||||
if (!tasks || tasks.length === 0) {
|
||||
console.warn('All appropriate tasks found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var taskPromises = tasks.map(updatetask);
|
||||
var lasttask = tasks[tasks.length - 1];
|
||||
|
||||
return Promise.all(taskPromises)
|
||||
.then(function () {
|
||||
processtasks(lasttask._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updatetask (task) {
|
||||
count++;
|
||||
var set = {'yesterDaily': true};
|
||||
|
||||
dbTasks.update({_id: task._id}, {$set:set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + task._id);
|
||||
if (task._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' tasks processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processtasks;
|
||||
@@ -0,0 +1,109 @@
|
||||
var migrationName = 'UserFromProdToTest';
|
||||
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
|
||||
var authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* This migraition will copy user data from prod to test
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var testConnectionSting = ''; // FOR TEST DATABASE
|
||||
var usersTest = monk(testConnectionSting).get('users', { castIds: false });
|
||||
var groupsTest = monk(testConnectionSting).get('groups', { castIds: false });
|
||||
var challengesTest = monk(testConnectionSting).get('challenges', { castIds: false });
|
||||
var tasksTest = monk(testConnectionSting).get('tasks', { castIds: false });
|
||||
|
||||
var monk2 = require('monk');
|
||||
var liveConnectString = ''; // FOR TEST DATABASE
|
||||
var userLive = monk2(liveConnectString).get('users', { castIds: false });
|
||||
var groupsLive = monk2(liveConnectString).get('groups', { castIds: false });
|
||||
var challengesLive = monk2(liveConnectString).get('challenges', { castIds: false });
|
||||
var tasksLive = monk2(liveConnectString).get('tasks', { castIds: false });
|
||||
|
||||
import uniq from 'lodash/uniq';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
// Variabls for updating
|
||||
let userIds = [
|
||||
'206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
|
||||
];
|
||||
|
||||
let groupIds = [];
|
||||
let challengeIds = [];
|
||||
let tasksIds = [];
|
||||
|
||||
async function processUsers () {
|
||||
let userPromises = [];
|
||||
//{_id: {$in: userIds}}
|
||||
|
||||
return userLive.find({guilds: 'b0764d64-8276-45a1-afa5-5ca9a5c64ca0'})
|
||||
.each((user, {close, pause, resume}) => {
|
||||
if (user.guilds.length > 0) groupIds = groupIds.concat(user.guilds);
|
||||
if (user.party._id) groupIds.push(user.party._id);
|
||||
if (user.challenges.length > 0) challengeIds = challengeIds.concat(user.challenges);
|
||||
if (user.tasksOrder.rewards.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.rewards);
|
||||
if (user.tasksOrder.todos.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.todos);
|
||||
if (user.tasksOrder.dailys.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.dailys);
|
||||
if (user.tasksOrder.habits.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.habits);
|
||||
|
||||
let userPromise = usersTest.update({'_id': user._id}, user, {upsert:true});
|
||||
userPromises.push(userPromise);
|
||||
}).then(() => {
|
||||
return Bluebird.all(userPromises);
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Done User");
|
||||
});
|
||||
}
|
||||
|
||||
function processGroups () {
|
||||
let promises = [];
|
||||
let groupsToQuery = uniq(groupIds);
|
||||
return groupsLive.find({_id: {$in: groupsToQuery}})
|
||||
.each((group, {close, pause, resume}) => {
|
||||
let promise = groupsTest.update({_id: group._id}, group, {upsert:true});
|
||||
promises.push(promise);
|
||||
}).then(() => {
|
||||
return Bluebird.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Done Group");
|
||||
});
|
||||
}
|
||||
|
||||
function processChallenges () {
|
||||
let promises = [];
|
||||
let challengesToQuery = uniq(challengeIds);
|
||||
return challengesLive.find({_id: {$in: challengesToQuery}})
|
||||
.each((challenge, {close, pause, resume}) => {
|
||||
let promise = challengesTest.update({_id: challenge._id}, challenge, {upsert:true});
|
||||
promises.push(promise);
|
||||
}).then(() => {
|
||||
return Bluebird.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Done Challenge");
|
||||
});
|
||||
}
|
||||
|
||||
function processTasks () {
|
||||
let promises = [];
|
||||
let tasksToQuery = uniq(tasksIds);
|
||||
return tasksLive.find({_id: {$in: tasksToQuery}})
|
||||
.each((task, {close, pause, resume}) => {
|
||||
let promise = tasksTest.update({_id: task._id}, task, {upsert:true});
|
||||
promises.push(promise);
|
||||
}).then(() => {
|
||||
return Bluebird.all(promises);
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Done Tasks");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = async function prodToTest () {
|
||||
await processUsers();
|
||||
await processGroups();
|
||||
await processChallenges();
|
||||
await processTasks();
|
||||
};
|
||||
Generated
+951
-639
File diff suppressed because it is too large
Load Diff
+22
-12
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.89.0",
|
||||
"version": "3.104.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -13,10 +13,12 @@
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"axios": "^0.15.3",
|
||||
"axios": "^0.16.0",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
@@ -29,13 +31,14 @@
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "^4.0.0-alpha.6",
|
||||
"bootstrap-vue": "^0.16.1",
|
||||
"bower": "~1.3.12",
|
||||
"browserify": "~12.0.1",
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.26.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
@@ -66,6 +69,7 @@
|
||||
"gulp-uglify": "^1.4.2",
|
||||
"gulp.spritesmith": "^4.1.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"hellojs": "^1.15.1",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"image-size": "~0.3.2",
|
||||
"in-app-purchase": "^1.1.6",
|
||||
@@ -96,7 +100,7 @@
|
||||
"postcss-easy-import": "^2.0.0",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta11",
|
||||
"pug": "^2.0.0-beta.12",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.74.0",
|
||||
@@ -108,6 +112,9 @@
|
||||
"shelljs": "^0.7.6",
|
||||
"stripe": "^4.2.0",
|
||||
"superagent": "^3.4.3",
|
||||
"svg-inline-loader": "^0.7.1",
|
||||
"svg-url-loader": "^2.0.2",
|
||||
"svgo-loader": "^1.2.1",
|
||||
"universal-analytics": "~0.3.2",
|
||||
"url-loader": "^0.5.7",
|
||||
"useragent": "^2.1.9",
|
||||
@@ -119,11 +126,12 @@
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-style-loader": "^2.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^2.6.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.1.0",
|
||||
"winston-loggly-bulk": "^1.4.2",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
"private": true,
|
||||
@@ -133,7 +141,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit",
|
||||
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
@@ -150,14 +158,15 @@
|
||||
"test:nodemon": "gulp test:nodemon",
|
||||
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "node webpack/build.js",
|
||||
"client:dev": "gulp bootstrap && node webpack/dev-server.js",
|
||||
"client:build": "gulp bootstrap && node webpack/build.js",
|
||||
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
|
||||
"client:e2e": "node test/client/e2e/runner.js",
|
||||
"client:test": "npm run client:unit && npm run client:e2e",
|
||||
"start": "gulp run:dev",
|
||||
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
|
||||
"postinstall": "bower --config.interactive=false install -f && gulp build && npm run client:build",
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
@@ -167,7 +176,7 @@
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"cross-env": "^3.1.4",
|
||||
"cross-env": "^4.0.0",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
@@ -191,7 +200,7 @@
|
||||
"karma-mocha": "^0.2.0",
|
||||
"karma-mocha-reporter": "^1.1.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-sinon-chai": "^1.2.0",
|
||||
"karma-sinon-chai": "~1.2.0",
|
||||
"karma-sinon-stub-promise": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.24",
|
||||
@@ -205,6 +214,7 @@
|
||||
"nightwatch": "^0.9.12",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"require-again": "^2.0.0",
|
||||
"rewire": "^2.3.3",
|
||||
"selenium-server": "^3.0.1",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require("babel-register");
|
||||
require("babel-polyfill");
|
||||
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
|
||||
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
|
||||
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
|
||||
@@ -7,10 +9,10 @@ var path = require('path');
|
||||
var nconf = require('nconf');
|
||||
var _ = require('lodash');
|
||||
var paypal = require('paypal-rest-sdk');
|
||||
var blocks = require('../../../../common').content.subscriptionBlocks;
|
||||
var blocks = require('../website/common').content.subscriptionBlocks;
|
||||
var live = nconf.get('PAYPAL:mode')=='live';
|
||||
|
||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
|
||||
|
||||
var OP = 'create'; // list create update remove
|
||||
|
||||
@@ -49,6 +51,8 @@ _.each(blocks, function(block){
|
||||
});
|
||||
})
|
||||
|
||||
// @TODO: Add cli library for this
|
||||
|
||||
switch(OP) {
|
||||
case "list":
|
||||
paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){
|
||||
@@ -91,4 +95,17 @@ switch(OP) {
|
||||
});
|
||||
break;
|
||||
case "remove": break;
|
||||
|
||||
case 'create-webprofile':
|
||||
let webexpinfo = {
|
||||
"name": "HabiticaProfile",
|
||||
"input_fields": {
|
||||
"no_shipping": 1,
|
||||
},
|
||||
};
|
||||
|
||||
paypal.webProfile.create(webexpinfo, (error, result) => {
|
||||
console.log(error, result)
|
||||
})
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).
|
||||
@@ -1,123 +0,0 @@
|
||||
# So you want to write API integration tests?
|
||||
|
||||
@TODO rewrite
|
||||
|
||||
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
|
||||
|
||||
## What is this?
|
||||
|
||||
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
|
||||
|
||||
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
|
||||
|
||||
## How to run the tests
|
||||
|
||||
First, install gulp.
|
||||
|
||||
```bash
|
||||
$ npm install -g gulp
|
||||
```
|
||||
|
||||
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
|
||||
|
||||
```bash
|
||||
$ gulp test:api-v2
|
||||
```
|
||||
|
||||
It may take a little while to get going, since it requires the web server to start up before the tests can run.
|
||||
|
||||
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
|
||||
|
||||
```bash
|
||||
$ gulp test:api-v2:watch
|
||||
```
|
||||
|
||||
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
|
||||
|
||||
```bash
|
||||
$ gulp test:api:safe
|
||||
```
|
||||
|
||||
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
|
||||
|
||||
```bash
|
||||
$ gulp test:nodemon
|
||||
```
|
||||
|
||||
And run your tests in another pane:
|
||||
|
||||
```bash
|
||||
$ mocha path/to/file.js
|
||||
|
||||
# Mark a test with the `.only` attribute
|
||||
$ mocha
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
|
||||
|
||||
Each test should:
|
||||
|
||||
* encompase a single route
|
||||
* begin with the REST parameter for the route
|
||||
* display the full name of the route, swapping out `/` for `_`
|
||||
* end with test.js
|
||||
|
||||
So, for the `POST` route `/groups/:id/leave`, it would be
|
||||
|
||||
```bash
|
||||
POST-groups_id_leave.test.js
|
||||
```
|
||||
|
||||
## Promises
|
||||
|
||||
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
|
||||
|
||||
```js
|
||||
it('does something', () => {
|
||||
let user;
|
||||
|
||||
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
|
||||
user = _user;
|
||||
|
||||
return user.post('/groups', {
|
||||
type: 'party',
|
||||
});
|
||||
}).then((party) => { // the result of the promise above is the argument of the function
|
||||
return user.put(`/groups/${party._id}`, {
|
||||
name: 'My party',
|
||||
});
|
||||
}).then((result) => {
|
||||
return user.get('/groups/party');
|
||||
}).then((party) => {
|
||||
expect(party.name).to.eql('My party');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
|
||||
|
||||
```js
|
||||
it('makes the party creator the leader automatically', () => {
|
||||
return expect(user.post('/groups', {
|
||||
type: 'party',
|
||||
})).to.eventually.have.deep.property('leader._id', user._id);
|
||||
});
|
||||
```
|
||||
|
||||
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
|
||||
|
||||
```js
|
||||
it('returns an error', () => {
|
||||
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
text: t('messageGroupNotFound'),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!
|
||||
@@ -1,4 +0,0 @@
|
||||
# How to run tests:
|
||||
|
||||
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
|
||||
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other
|
||||
@@ -304,5 +304,15 @@ describe('POST /challenges', () => {
|
||||
|
||||
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
|
||||
});
|
||||
|
||||
it('awards achievement if this is creator\'s first challenge', async () => {
|
||||
await groupLeader.post('/challenges', {
|
||||
group: group._id,
|
||||
name: 'Test Challenge',
|
||||
shortName: 'TC Label',
|
||||
});
|
||||
groupLeader = await groupLeader.sync();
|
||||
expect(groupLeader.achievements.joinedChallenge).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,5 +123,12 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
|
||||
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
|
||||
});
|
||||
|
||||
it('awards achievement if this is user\'s first challenge', async () => {
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await authorizedUser.sync();
|
||||
expect(authorizedUser.achievements.joinedChallenge).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+22
-2
@@ -100,8 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
||||
await sleep(0.5);
|
||||
|
||||
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
|
||||
expect(winningUser.notifications.length).to.equal(1);
|
||||
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
|
||||
expect(winningUser.notifications.length).to.equal(2); // 2 because winningUser just joined the challenge, which now awards an achievement
|
||||
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
|
||||
});
|
||||
|
||||
it('gives winner gems as reward', async () => {
|
||||
@@ -114,6 +114,26 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
||||
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4);
|
||||
});
|
||||
|
||||
it('doesn\'t gives winner gems if group policy prevents it', async () => {
|
||||
let oldBalance = winningUser.balance;
|
||||
let oldLeaderBalance = (await groupLeader.sync()).balance;
|
||||
|
||||
await winningUser.update({
|
||||
'purchased.plan.customerId': 'group-plan',
|
||||
});
|
||||
await group.update({
|
||||
'leaderOnly.getGems': true,
|
||||
'purchased.plan.customerId': 123,
|
||||
});
|
||||
|
||||
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance);
|
||||
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldLeaderBalance + challenge.prize / 4);
|
||||
});
|
||||
|
||||
it('doesn\'t refund gems to group leader', async () => {
|
||||
let oldBalance = (await groupLeader.sync()).balance;
|
||||
|
||||
|
||||
@@ -84,6 +84,10 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
});
|
||||
await user.post(`/groups/${privateGroup._id}/invite`, {
|
||||
uuids: [anotherUser._id],
|
||||
});
|
||||
await anotherUser.post(`/groups/${privateGroup._id}/join`);
|
||||
let { message } = await user.post(`/groups/${privateGroup._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
let flagResult = await admin.post(`/groups/${privateGroup._id}/chat/${message.id}/flag`);
|
||||
@@ -91,7 +95,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
expect(flagResult.flags[admin._id]).to.equal(true);
|
||||
expect(flagResult.flagCount).to.equal(5);
|
||||
|
||||
let groupWithFlags = await user.get(`/groups/${privateGroup._id}`);
|
||||
let groupWithFlags = await anotherUser.get(`/groups/${privateGroup._id}`);
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(messageToCheck).to.not.exist;
|
||||
@@ -125,4 +129,20 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
message: t('messageGroupChatFlagAlreadyReported'),
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a hidden message to the original poster', async () => {
|
||||
let { message } = await user.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
await admin.post(`/groups/${group._id}/chat/${message.id}/flag`);
|
||||
|
||||
let groupWithFlags = await user.get(`/groups/${group._id}`);
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(messageToCheck).to.exist;
|
||||
|
||||
let auGroupWithFlags = await anotherUser.get(`/groups/${group._id}`);
|
||||
let auMessageToCheck = find(auGroupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(auMessageToCheck).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,17 @@ import {
|
||||
sleep,
|
||||
server,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
TAVERN_ID,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
let user, groupWithChat, userWithChatRevoked, member;
|
||||
let user, groupWithChat, member, additionalMember;
|
||||
let testMessage = 'Test Message';
|
||||
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
|
||||
|
||||
before(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
@@ -22,8 +28,8 @@ describe('POST /chat', () => {
|
||||
|
||||
user = groupLeader;
|
||||
groupWithChat = group;
|
||||
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
|
||||
member = members[0];
|
||||
additionalMember = members[1];
|
||||
});
|
||||
|
||||
it('Returns an error when no message is provided', async () => {
|
||||
@@ -62,10 +68,101 @@ describe('POST /chat', () => {
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Your chat privileges have been revoked.',
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
context('banned word', () => {
|
||||
it('returns an error when chat message contains a banned word in tavern', async () => {
|
||||
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('bannedWordUsed'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when word is part of a phrase', async () => {
|
||||
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
|
||||
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('bannedWordUsed'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when word is surrounded by non alphabet characters', async () => {
|
||||
let wordInPhrase = `_!${testBannedWordMessage}@_`;
|
||||
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('bannedWordUsed'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not error when bad word is suffix of a word', async () => {
|
||||
let wordAsSuffix = `prefix${testBannedWordMessage}`;
|
||||
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when bad word is prefix of a word', async () => {
|
||||
let wordAsPrefix = `${testBannedWordMessage}suffix`;
|
||||
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a chat message containing a banned word to a party', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'public guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'private guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,4 +270,30 @@ describe('POST /chat', () => {
|
||||
expect(message.message.id).to.exist;
|
||||
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
|
||||
});
|
||||
|
||||
context('Spam prevention', () => {
|
||||
it('Returns an error when the user has been posting too many messages', async () => {
|
||||
// Post as many messages are needed to reach the spam limit
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||
expect(result.message.id).to.exist;
|
||||
}
|
||||
|
||||
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupChatSpam'),
|
||||
});
|
||||
});
|
||||
|
||||
it('contributor should not receive spam alert', async () => {
|
||||
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
|
||||
|
||||
// Post 1 more message than the spam limit to ensure they do not reach the limit
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
|
||||
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||
expect(result.message.id).to.exist;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /group/:groupId/remove-manager', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let nonManager;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === groupToUpdate._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
nonManager = members[0];
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to add member', async () => {
|
||||
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when manager does not exist', async () => {
|
||||
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonManager._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('userIsNotManager'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a leader to remove managers', async () => {
|
||||
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes group approval notifications from a manager that is removed', async () => {
|
||||
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
|
||||
let memberTasks = await leader.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
await nonLeader.sync();
|
||||
|
||||
expect(nonLeader.notifications.length).to.equal(0);
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,18 @@ describe('POST /group', () => {
|
||||
expect(updatedUser.guilds).to.include(guild._id);
|
||||
});
|
||||
|
||||
it('awards the Joined Guild achievement', async () => {
|
||||
await user.post('/groups', {
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
|
||||
let updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.achievements.joinedGuild).to.eql(true);
|
||||
});
|
||||
|
||||
context('public guild', () => {
|
||||
it('creates a group', async () => {
|
||||
let groupName = 'Test Public Guild';
|
||||
|
||||
@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
|
||||
|
||||
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await joiningUser.post(`/groups/${publicGuild._id}/join`);
|
||||
|
||||
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a private guild', () => {
|
||||
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[1].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
let guild;
|
||||
let member;
|
||||
let member2;
|
||||
let adminUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
|
||||
@@ -28,6 +29,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
invitedUser = invitees[0];
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
});
|
||||
|
||||
context('All Groups', () => {
|
||||
@@ -42,7 +44,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when user is a non-leader member of a group', async () => {
|
||||
it('returns an error when user is a non-leader member of a group and not an admin', async () => {
|
||||
expect(member2.post(`/groups/${guild._id}/removeMember/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
@@ -87,7 +89,30 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
|
||||
let invitedUserWithoutInvite = await invitedUser.get('/user');
|
||||
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
|
||||
});
|
||||
|
||||
it('allows an admin to remove other members', async () => {
|
||||
await adminUser.post(`/groups/${guild._id}/removeMember/${member._id}`);
|
||||
let memberRemoved = await member.get('/user');
|
||||
|
||||
expect(memberRemoved.guilds.indexOf(guild._id)).eql(-1);
|
||||
});
|
||||
|
||||
it('allows an admin to remove other invites', async () => {
|
||||
await adminUser.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
|
||||
|
||||
let invitedUserWithoutInvite = await invitedUser.get('/user');
|
||||
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
|
||||
});
|
||||
|
||||
it('does not allow an admin to remove a leader', async () => {
|
||||
expect(adminUser.post(`/groups/${guild._id}/removeMember/${leader._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: t('cannotRemoveCurrentLeader'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /group/:groupId/add-manager', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let nonMember;
|
||||
|
||||
context('Guilds', () => {
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
nonMember = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to add member', async () => {
|
||||
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when trying to promote a non member', async () => {
|
||||
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonMember._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('userMustBeMember'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a leader to add managers', async () => {
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
context('Party', () => {
|
||||
let party, partyLeader, partyNonLeader;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
party = group;
|
||||
partyLeader = groupLeader;
|
||||
partyNonLeader = members[0];
|
||||
});
|
||||
|
||||
it('allows leader of party to add managers', async () => {
|
||||
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
|
||||
managerId: partyNonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('PUT /group', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let leader, nonLeader, groupToUpdate, adminUser;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let groupUpdatedName = 'Test Public Guild Updated';
|
||||
@@ -18,13 +19,13 @@ describe('PUT /group', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to update', async () => {
|
||||
it('returns an error when a user that is not an admin or group leader tries to update', async () => {
|
||||
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
@@ -44,6 +45,15 @@ describe('PUT /group', () => {
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
|
||||
it('allows an admin to update a guild', async () => {
|
||||
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
});
|
||||
expect(updatedGroup.leader._id).to.eql(leader._id);
|
||||
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
|
||||
it('allows a leader to change leaders', async () => {
|
||||
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /members/:toUserId/objections/:interaction', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(
|
||||
user.get('/members/invalidUUID/objections/send-private-message')
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(
|
||||
user.get(`/members/${dummyId}/objections/send-private-message`)
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', {userId: dummyId}),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing interactions', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/hug-a-whole-forest-of-trees`)
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array if there are no objections', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/send-private-message`)
|
||||
).to.eventually.be.fulfilled.and.eql([]);
|
||||
});
|
||||
|
||||
it('returns an array of objections if any exist', async () => {
|
||||
let receiver = await generateUser({'inbox.blocks': [user._id]});
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/send-private-message`)
|
||||
).to.eventually.be.fulfilled.and.eql([
|
||||
t('notAuthorizedToSendMessageToThisUser'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,20 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked', async () => {
|
||||
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(userWithChatRevoked.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a private message to a user', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user is not found', async () => {
|
||||
it('returns error when recipient is not found', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
@@ -55,7 +55,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user attempts to send gems to themselves', async () => {
|
||||
it('returns error when user attempts to send gems to themselves', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
@@ -67,6 +67,64 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when recipient has blocked the sender', async () => {
|
||||
let receiverWhoBlocksUser = await generateUser({'inbox.blocks': [userToSendMessage._id]});
|
||||
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiverWhoBlocksUser._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notAuthorizedToSendMessageToThisUser'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when sender has blocked recipient', async () => {
|
||||
let sender = await generateUser({'inbox.blocks': [receiver._id]});
|
||||
|
||||
await expect(sender.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notAuthorizedToSendMessageToThisUser'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked', async () => {
|
||||
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
|
||||
await expect(userWithChatRevoked.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works when only the recipient\'s chat privileges are revoked', async () => {
|
||||
let receiverWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiverWithChatRevoked._id,
|
||||
})).to.eventually.be.fulfilled;
|
||||
|
||||
let updatedReceiver = await receiverWithChatRevoked.get('/user');
|
||||
let updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
expect(updatedReceiver.balance).to.equal(gemAmount / 4);
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns error when there is no gemAmount', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
@@ -144,7 +202,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not requrie a message', async () => {
|
||||
it('does not require a message', async () => {
|
||||
await userToSendMessage.post('/members/transfer-gems', {
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /shops/backgrounds', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns a valid shop object', async () => {
|
||||
let shop = await user.get('/shops/backgrounds');
|
||||
expect(shop.identifier).to.equal('backgroundShop');
|
||||
expect(shop.text).to.eql(t('backgroundShop'));
|
||||
expect(shop.notes).to.eql(t('backgroundShopText'));
|
||||
expect(shop.imageName).to.equal('background_shop');
|
||||
expect(shop.sets).to.be.an('array');
|
||||
|
||||
let sets = shop.sets.map(set => set.identifier);
|
||||
expect(sets).to.include('incentiveBackgrounds');
|
||||
expect(sets).to.include('backgrounds062014');
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
@@ -127,4 +128,106 @@ describe('GET /tasks/user', () => {
|
||||
let allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
|
||||
expect(allCompletedTodos.length).to.equal(numberOfTodos);
|
||||
});
|
||||
|
||||
it('returns dailies with isDue for the date specified', async () => {
|
||||
// @TODO Add required format
|
||||
let startDate = moment().subtract('1', 'days').toISOString();
|
||||
let createdTasks = await user.post('/tasks/user', [
|
||||
{
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate,
|
||||
frequency: 'daily',
|
||||
everyX: 2,
|
||||
},
|
||||
]);
|
||||
let dailys = await user.get('/tasks/user?type=dailys');
|
||||
|
||||
expect(dailys.length).to.be.at.least(1);
|
||||
expect(dailys[0]._id).to.equal(createdTasks._id);
|
||||
expect(dailys[0].isDue).to.be.false;
|
||||
|
||||
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${startDate}`);
|
||||
expect(dailys2[0]._id).to.equal(createdTasks._id);
|
||||
expect(dailys2[0].isDue).to.be.true;
|
||||
});
|
||||
|
||||
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
let timezone = 420;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
});
|
||||
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate,
|
||||
frequency: 'daily',
|
||||
everyX: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
let today = moment().format('YYYY-MM-DD');
|
||||
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
|
||||
expect(dailys[0].isDue).to.be.true;
|
||||
|
||||
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
|
||||
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
|
||||
expect(dailys2[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
|
||||
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
let timezone = 240;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
});
|
||||
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate,
|
||||
frequency: 'daily',
|
||||
everyX: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
let today = moment().format('YYYY-MM-DD');
|
||||
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
|
||||
expect(dailys[0].isDue).to.be.true;
|
||||
|
||||
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
|
||||
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
|
||||
expect(dailys2[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
|
||||
let timezone = 540;
|
||||
await user.update({
|
||||
'preferences.dayStart': 0,
|
||||
'preferences.timezoneOffset': timezone,
|
||||
});
|
||||
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
|
||||
await user.post('/tasks/user', [
|
||||
{
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate,
|
||||
frequency: 'daily',
|
||||
everyX: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
let today = moment().format('YYYY-MM-DD');
|
||||
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
|
||||
expect(dailys[0].isDue).to.be.true;
|
||||
|
||||
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
|
||||
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
|
||||
expect(dailys2[0].isDue).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -208,6 +208,20 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(task.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('computes isDue', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.isDue).to.equal(true);
|
||||
});
|
||||
|
||||
it('computes nextDue', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('scores up daily even if it is already completed'); // Yes?
|
||||
|
||||
it('scores down daily even if it is already uncompleted'); // Yes?
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /tasks/unlink-all/:challengeId', () => {
|
||||
let user;
|
||||
let guild;
|
||||
let challenge;
|
||||
let tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
guild = await generateGroup(user);
|
||||
challenge = await generateChallenge(user, guild);
|
||||
});
|
||||
|
||||
it('fails if no keep query', async () => {
|
||||
await expect(user.post(`/tasks/unlink-all/${challenge._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if invalid challenge id', async () => {
|
||||
await expect(user.post('/tasks/unlink-all/123?keep=remove-all'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on an unbroken challenge', async () => {
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
await expect(user.post(`/tasks/unlink-all/${challenge._id}?keep=remove-all`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cantOnlyUnlinkChalTask'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks all tasks from a challenge and deletes them on keep=remove-all', async () => {
|
||||
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.habit);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.reward);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.todo);
|
||||
await user.del(`/challenges/${challenge._id}`);
|
||||
const response = await user.post(`/tasks/unlink-all/${challenge._id}?keep=remove-all`);
|
||||
expect(response).to.eql({});
|
||||
|
||||
await expect(user.get(`/tasks/${daily._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks a task from a challenge on keep=keep-all', async () => {
|
||||
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
const anotherUser = await generateUser();
|
||||
await user.post(`/groups/${guild._id}/invite`, {
|
||||
uuids: [anotherUser._id],
|
||||
});
|
||||
// Have the second user join the group and challenge
|
||||
await anotherUser.post(`/groups/${guild._id}/join`);
|
||||
await anotherUser.post(`/challenges/${challenge._id}/join`);
|
||||
// Have the leader delete the challenge and unlink the tasks
|
||||
await user.del(`/challenges/${challenge._id}`);
|
||||
await user.post(`/tasks/unlink-all/${challenge._id}?keep=keep-all`);
|
||||
// Get the second task for the second user
|
||||
const [, anotherUserTask] = await anotherUser.get('/tasks/user');
|
||||
// Expect the second user to still have the task, but unlinked
|
||||
expect(anotherUserTask.challenge).to.eql({
|
||||
taskId: daily._id,
|
||||
id: challenge._id,
|
||||
shortName: challenge.shortName,
|
||||
broken: 'CHALLENGE_DELETED',
|
||||
winner: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/unlink-one/:taskId', () => {
|
||||
let user;
|
||||
let guild;
|
||||
let challenge;
|
||||
let tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
guild = await generateGroup(user);
|
||||
challenge = await generateChallenge(user, guild);
|
||||
});
|
||||
|
||||
it('fails if no keep query', async () => {
|
||||
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
await expect(user.post(`/tasks/unlink-one/${daily._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if invalid task id', async () => {
|
||||
await expect(user.post('/tasks/unlink-one/123?keep=remove'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.post(`/tasks/unlink-one/${generateUUID()}?keep=keep`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task unlinked to challenge', async () => {
|
||||
let daily = await user.post('/tasks/user', tasksToTest.daily);
|
||||
await expect(user.post(`/tasks/unlink-one/${daily._id}?keep=keep`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cantOnlyUnlinkChalTask'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on unbroken challenge', async () => {
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
let [daily] = await user.get('/tasks/user');
|
||||
await expect(user.post(`/tasks/unlink-one/${daily._id}?keep=keep`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cantOnlyUnlinkChalTask'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks a task from a challenge and saves it on keep=keep', async () => {
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
let [, daily] = await user.get('/tasks/user');
|
||||
await user.del(`/challenges/${challenge._id}`);
|
||||
await user.post(`/tasks/unlink-one/${daily._id}?keep=keep`);
|
||||
[, daily] = await user.get('/tasks/user');
|
||||
expect(daily.challenge).to.eql({});
|
||||
});
|
||||
|
||||
it('unlinks a task from a challenge and deletes it on keep=remove', async () => {
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
|
||||
let [, daily] = await user.get('/tasks/user');
|
||||
await user.del(`/challenges/${challenge._id}`);
|
||||
await user.post(`/tasks/unlink-one/${daily._id}?keep=remove`);
|
||||
const tasks = await user.get('/tasks/user');
|
||||
// Only the default task should remain
|
||||
expect(tasks.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
@@ -133,6 +133,7 @@ describe('POST /tasks/user', () => {
|
||||
expect(task.completed).to.equal(false);
|
||||
expect(task.streak).not.to.equal('never');
|
||||
expect(task.value).not.to.equal(324);
|
||||
expect(task.yesterDaily).to.equal(true);
|
||||
});
|
||||
|
||||
it('ignores invalid fields', async () => {
|
||||
@@ -509,6 +510,8 @@ describe('POST /tasks/user', () => {
|
||||
expect(task.daysOfMonth).to.eql([15]);
|
||||
expect(task.weeksOfMonth).to.eql([3]);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
expect(task.isDue).to.be.true;
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('creates multiple dailys', async () => {
|
||||
@@ -613,6 +616,18 @@ describe('POST /tasks/user', () => {
|
||||
expect((new Date(task.startDate)).getDay()).to.eql(today);
|
||||
});
|
||||
|
||||
it('returns an error if the start date is empty', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('can create checklists', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
@@ -395,12 +396,17 @@ describe('PUT /tasks/:id', () => {
|
||||
notes: 'some new notes',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
yesterDaily: false,
|
||||
startDate: moment().add(1, 'days').toDate(),
|
||||
});
|
||||
|
||||
expect(savedDaily.text).to.eql('some new text');
|
||||
expect(savedDaily.notes).to.eql('some new notes');
|
||||
expect(savedDaily.frequency).to.eql('daily');
|
||||
expect(savedDaily.everyX).to.eql(5);
|
||||
expect(savedDaily.isDue).to.be.false;
|
||||
expect(savedDaily.nextDue.length).to.eql(6);
|
||||
expect(savedDaily.yesterDaily).to.be.false;
|
||||
});
|
||||
|
||||
it('can update checklists (replace it)', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
describe('Groups DELETE /tasks/:id', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
@@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a manager to delete a group task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.del(`/tasks/${task._id}`);
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
|
||||
@@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
|
||||
let approvals = await user.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
});
|
||||
|
||||
it('allows managers to get a list of task that need approval', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member._id,
|
||||
});
|
||||
|
||||
let approvals = await member.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let user, guild, member, task;
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
@@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('allows a manager to approve an assigned user', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||
expect(member.notifications[1].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||
|
||||
expect(syncedTask.group.approval.approved).to.be.true;
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('removes approval pending notifications from managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(member2.notifications.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('prevents double approval on a task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
let user, guild, member, task;
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
@@ -51,18 +52,55 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('sends notifications to all managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('errors when approval has already been requested', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/group/:groupid', () => {
|
||||
let user, guild;
|
||||
let user, guild, manager;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
guild = await generateGroup(user, {type: 'guild'});
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
manager = members[0];
|
||||
});
|
||||
|
||||
it('returns error when group is not found', async () => {
|
||||
@@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
|
||||
expect(task.everyX).to.eql(5);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
});
|
||||
|
||||
it('allows a manager to add a group task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: manager._id,
|
||||
});
|
||||
|
||||
let task = await manager.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||
expect(task.text).to.eql('test habit');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('habit');
|
||||
expect(task.up).to.eql(false);
|
||||
expect(task.down).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:taskId', () => {
|
||||
describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
@@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
|
||||
expect(member1SyncedTask).to.exist;
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('allows a manager to assign tasks', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('allows a manager to unassign a user from a task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(member2SyncedTask.up).to.eql(false);
|
||||
expect(member2SyncedTask.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.put(`/tasks/${task._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('DELETE /user', () => {
|
||||
let user;
|
||||
@@ -25,7 +26,7 @@ describe('DELETE /user', () => {
|
||||
user = await generateUser({balance: 10});
|
||||
});
|
||||
|
||||
it('returns an errors if password is wrong', async () => {
|
||||
it('returns an error if password is wrong', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: 'wrong-password',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
@@ -35,6 +36,33 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if password is not supplied', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
let feedbackText = 'spam feedback ';
|
||||
let feedback = feedbackText;
|
||||
while (feedback.length < 10000) {
|
||||
feedback = feedback + feedbackText;
|
||||
}
|
||||
|
||||
await expect(user.del('/user', {
|
||||
password,
|
||||
feedback,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if user has active subscription', async () => {
|
||||
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
|
||||
|
||||
@@ -96,6 +124,32 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
let feedback = 'Reasons for Deletion';
|
||||
await user.del('/user', {
|
||||
password,
|
||||
feedback,
|
||||
});
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('does not send email if no feedback is supplied', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
expect(email.sendTxn).to.not.be.called;
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
|
||||
@@ -221,6 +221,27 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(syncedGroupTask.value).to.equal(0);
|
||||
});
|
||||
|
||||
it('increases both user\'s achievement values', async () => {
|
||||
let party = await createAndPopulateGroup({
|
||||
members: 1,
|
||||
});
|
||||
let leader = party.groupLeader;
|
||||
let recipient = party.members[0];
|
||||
await leader.update({'stats.gp': 10});
|
||||
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
|
||||
await leader.sync();
|
||||
await recipient.sync();
|
||||
expect(leader.achievements.birthday).to.equal(1);
|
||||
expect(recipient.achievements.birthday).to.equal(1);
|
||||
});
|
||||
|
||||
it('only increases user\'s achievement one if target == caster', async () => {
|
||||
await user.update({'stats.gp': 10});
|
||||
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
|
||||
await user.sync();
|
||||
expect(user.achievements.birthday).to.equal(1);
|
||||
});
|
||||
|
||||
// TODO find a way to have sinon working in integration tests
|
||||
// it doesn't work when tests are running separately from server
|
||||
it('passes correct target to spell when targetType === \'task\'');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
@@ -31,4 +32,70 @@ describe('POST /user/purchase/:type/:key', () => {
|
||||
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
});
|
||||
|
||||
it('can convert gold to gems if subscribed', async () => {
|
||||
let oldBalance = user.balance;
|
||||
await user.update({
|
||||
'purchased.plan.customerId': 'group-plan',
|
||||
'stats.gp': 1000,
|
||||
});
|
||||
await user.post('/user/purchase/gems/gem');
|
||||
await user.sync();
|
||||
expect(user.balance).to.equal(oldBalance + 0.25);
|
||||
});
|
||||
|
||||
it('leader can convert gold to gems even if the group plan prevents it', async () => {
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
});
|
||||
await group.update({
|
||||
'leaderOnly.getGems': true,
|
||||
'purchased.plan.customerId': 123,
|
||||
});
|
||||
await groupLeader.sync();
|
||||
let oldBalance = groupLeader.balance;
|
||||
|
||||
await groupLeader.update({
|
||||
'purchased.plan.customerId': 'group-plan',
|
||||
'stats.gp': 1000,
|
||||
});
|
||||
await groupLeader.post('/user/purchase/gems/gem');
|
||||
|
||||
await groupLeader.sync();
|
||||
expect(groupLeader.balance).to.equal(oldBalance + 0.25);
|
||||
});
|
||||
|
||||
it('cannot convert gold to gems if the group plan prevents it', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
await group.update({
|
||||
'leaderOnly.getGems': true,
|
||||
'purchased.plan.customerId': 123,
|
||||
});
|
||||
let oldBalance = members[0].balance;
|
||||
|
||||
await members[0].update({
|
||||
'purchased.plan.customerId': 'group-plan',
|
||||
'stats.gp': 1000,
|
||||
});
|
||||
await expect(members[0].post('/user/purchase/gems/gem'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
|
||||
await members[0].sync();
|
||||
expect(members[0].balance).to.equal(oldBalance);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,7 +251,6 @@ describe('POST /user/auth/local/register', () => {
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,7 @@ describe('Amazon Payments', () => {
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
@@ -111,22 +112,52 @@ describe('Amazon Payments', () => {
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
amount = 16 / 4;
|
||||
await amzLib.checkout({gift, user, orderReferenceId, headers});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
@@ -525,6 +556,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
@@ -555,6 +587,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
@@ -593,6 +626,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
@@ -623,6 +657,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
@@ -57,7 +57,20 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if the user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await applePayments.verifyGemPurchase(user, receipt, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -74,6 +87,8 @@ describe('Apple Payments', () => {
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -79,6 +79,13 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month if user does not have purchased.plan.dateUpdated', () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
user.purchased.plan.dateUpdated = undefined;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not reset plan.gemsBought within the month', () => {
|
||||
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
||||
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
||||
@@ -313,6 +320,24 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
expect(user.stats.hp).to.equal(healthBefore);
|
||||
});
|
||||
|
||||
it('sets isDue for daily', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].isDue).to.be.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos', () => {
|
||||
@@ -358,6 +383,22 @@ describe('cron', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('computes isDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
it('computes nextDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
@@ -442,6 +483,25 @@ describe('cron', () => {
|
||||
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
|
||||
it('should do damage for only yesterday\'s dailies', () => {
|
||||
daysMissed = 3;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
};
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[1].startDate = moment(new Date()).subtract({days: 2});
|
||||
tasksByType.dailys[1].everyX = 2;
|
||||
tasksByType.dailys[1].frequency = 'daily';
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.hp).to.equal(48);
|
||||
});
|
||||
});
|
||||
|
||||
describe('habits', () => {
|
||||
|
||||
@@ -63,7 +63,21 @@ describe('Google Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -82,6 +96,8 @@ describe('Google Payments', () => {
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
@@ -504,6 +505,18 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
user.purchased.plan.extraMonths = 5;
|
||||
|
||||
@@ -653,5 +666,32 @@ describe('payments/index', () => {
|
||||
|
||||
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
|
||||
let planExpirationDate = new Date();
|
||||
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
|
||||
let mysteryItem = 'item';
|
||||
let mysteryItems = [mysteryItem];
|
||||
let consecutive = {
|
||||
trinkets: 3,
|
||||
};
|
||||
|
||||
// set expired plan with unused items
|
||||
plan.mysteryItems = mysteryItems;
|
||||
plan.consecutive = consecutive;
|
||||
plan.dateCreated = planExpirationDate;
|
||||
plan.dateTerminated = planExpirationDate;
|
||||
plan.customerId = null;
|
||||
|
||||
user.purchased.plan = plan;
|
||||
|
||||
await user.save();
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
|
||||
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('Canceling a subscription for group', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents non group leader from manging subscription', async () => {
|
||||
it('prevents non group leader from managing subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import stripeModule from 'stripe';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import * as sender from '../../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../../website/server/libs/payments';
|
||||
@@ -12,17 +13,24 @@ import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('Purchasing a subscription for group', () => {
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
|
||||
|
||||
let plan, group, user, data;
|
||||
let stripe = stripeModule('test');
|
||||
let groupLeaderName = 'sender';
|
||||
let groupName = 'test group';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.profile.name = groupLeaderName;
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
@@ -81,7 +89,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
it('creates a subscription', async () => {
|
||||
it('creates a group plan', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -157,7 +165,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
|
||||
});
|
||||
|
||||
it('sends an email to members of group', async () => {
|
||||
it('sends an email to member of group who was not a subscriber', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
@@ -169,11 +177,181 @@ describe('Purchasing a subscription for group', () => {
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
|
||||
]);
|
||||
// confirm that the other email sent is appropriate:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
|
||||
sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: { // eslint-disable-line camelcase
|
||||
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
|
||||
cycles_completed: 1, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledFourTimes;
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledFourTimes;
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
@@ -333,7 +511,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Android)');
|
||||
it('adds months to members with existing recurring subscription (iOs)');
|
||||
it('adds months to members with existing recurring subscription (iOS)');
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
|
||||
let recipient = new User();
|
||||
@@ -418,7 +596,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 8);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 9);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
|
||||
@@ -603,6 +781,64 @@ describe('Purchasing a subscription for group', () => {
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not modify a user with an Android subscription', async () => {
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not modify a user with an iOS subscription', async () => {
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('updates a user with a cancelled but active group subscription', async () => {
|
||||
plan.key = 'basic_earned';
|
||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
|
||||
@@ -61,23 +61,54 @@ describe('Paypal Payments', () => {
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
let link = await paypalPayments.checkout();
|
||||
let link = await paypalPayments.checkout({user: new User()});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(paypalPayments.checkout({gift}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the user cannot get gems', async () => {
|
||||
let user = new User();
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
@@ -447,6 +478,7 @@ describe('Paypal Payments', () => {
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,6 +496,7 @@ describe('Paypal Payments', () => {
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,57 @@ describe('Stripe Payments', () => {
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should error if user cannot get gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
@@ -73,16 +123,18 @@ describe('Stripe Payments', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -96,7 +148,6 @@ describe('Stripe Payments', () => {
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '400',
|
||||
@@ -683,6 +734,7 @@ describe('Stripe Payments', () => {
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -702,6 +754,7 @@ describe('Stripe Payments', () => {
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,13 @@ import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import validator from 'validator';
|
||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
SPAM_WINDOW_LENGTH,
|
||||
INVITES_LIMIT,
|
||||
model as Group,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
@@ -222,7 +228,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('applies damage only to participating members of party even under buggy conditions', async () => {
|
||||
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitrpg/issues/7653
|
||||
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitica/issues/7653
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
@@ -595,6 +601,99 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkChatSpam', () => {
|
||||
let testUser, testTime, tavern;
|
||||
let testUserID = '1';
|
||||
beforeEach(async () => {
|
||||
testTime = Date.now();
|
||||
|
||||
tavern = new Group({
|
||||
name: 'test tavern',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
tavern._id = TAVERN_ID;
|
||||
|
||||
testUser = {
|
||||
_id: testUserID,
|
||||
};
|
||||
});
|
||||
|
||||
function generateTestMessage (overrides = {}) {
|
||||
return Object.assign({}, {
|
||||
text: 'test message',
|
||||
uuid: testUserID,
|
||||
timestamp: testTime,
|
||||
}, overrides);
|
||||
}
|
||||
|
||||
it('group that is not the tavern returns false, while tavern returns true', async () => {
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
party.chat.push(generateTestMessage());
|
||||
}
|
||||
expect(party.checkChatSpam(testUser)).to.eql(false);
|
||||
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
tavern.chat.push(generateTestMessage());
|
||||
}
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(true);
|
||||
});
|
||||
|
||||
it('high enough contributor returns false', async () => {
|
||||
let highContributor = testUser;
|
||||
highContributor.contributor = {
|
||||
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
};
|
||||
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
tavern.chat.push(generateTestMessage());
|
||||
}
|
||||
|
||||
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
|
||||
});
|
||||
|
||||
it('chat with no messages returns false', async () => {
|
||||
expect(tavern.chat.length).to.eql(0);
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||
});
|
||||
|
||||
it('user has not reached limit but another one has returns false', async () => {
|
||||
let otherUserID = '2';
|
||||
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
|
||||
}
|
||||
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||
});
|
||||
|
||||
it('user messages is less than the limit returns false', async () => {
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
|
||||
tavern.chat.push(generateTestMessage());
|
||||
}
|
||||
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||
});
|
||||
|
||||
it('user has reached the message limit outside of window returns false', async () => {
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
|
||||
tavern.chat.push(generateTestMessage());
|
||||
}
|
||||
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
|
||||
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
|
||||
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||
});
|
||||
|
||||
it('user has posted too many messages in limit returns true', async () => {
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
tavern.chat.push(generateTestMessage());
|
||||
}
|
||||
|
||||
expect(tavern.checkChatSpam(testUser)).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#leaveGroup', () => {
|
||||
it('removes user from group quest', async () => {
|
||||
party.quest.members = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Bluebird from 'bluebird';
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
describe('User Model', () => {
|
||||
@@ -179,6 +180,75 @@ describe('User Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('canGetGems', () => {
|
||||
let user;
|
||||
let group;
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
let leader = new User();
|
||||
group = new Group({
|
||||
name: 'test',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
leader: leader._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true if user is not subscribed', async () => {
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if user is not subscribed with a group plan', async () => {
|
||||
user.purchased.plan.customerId = 123;
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if user is subscribed with a group plan', async () => {
|
||||
user.purchased.plan.customerId = 'group-plan';
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if user is part of a group', async () => {
|
||||
user.guilds.push(group._id);
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if user is part of a group with a subscription', async () => {
|
||||
user.guilds.push(group._id);
|
||||
user.purchased.plan.customerId = 'group-plan';
|
||||
group.purchased.plan.customerId = 123;
|
||||
await group.save();
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if leader is part of a group with a subscription and canGetGems: false', async () => {
|
||||
user.guilds.push(group._id);
|
||||
user.purchased.plan.customerId = 'group-plan';
|
||||
group.purchased.plan.customerId = 123;
|
||||
group.leader = user._id;
|
||||
group.leaderOnly.getGems = true;
|
||||
await group.save();
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true if user is part of a group with no subscription but canGetGems: false', async () => {
|
||||
user.guilds.push(group._id);
|
||||
user.purchased.plan.customerId = 'group-plan';
|
||||
group.leaderOnly.getGems = true;
|
||||
await group.save();
|
||||
expect(await user.canGetGems()).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false if user is part of a group with a subscription and canGetGems: false', async () => {
|
||||
user.guilds.push(group._id);
|
||||
user.purchased.plan.customerId = 'group-plan';
|
||||
group.purchased.plan.customerId = 123;
|
||||
group.leaderOnly.getGems = true;
|
||||
await group.save();
|
||||
expect(await user.canGetGems()).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('hasNotCancelled', () => {
|
||||
let user;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -112,6 +112,41 @@ describe('Groups Controller', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAbleToEditGroup', () => {
|
||||
var guild;
|
||||
|
||||
beforeEach(() => {
|
||||
user.contributor = {};
|
||||
guild = specHelper.newGroup({
|
||||
_id: 'unique-guild-id',
|
||||
type: 'guild',
|
||||
members: ['not-user-id'],
|
||||
$save: sandbox.spy(),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true if user is an admin', () => {
|
||||
guild.leader = 'not-user-id';
|
||||
user.contributor.admin = true;
|
||||
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
|
||||
});
|
||||
|
||||
it('returns true if user is group leader', () => {
|
||||
guild.leader = {_id: user._id}
|
||||
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
|
||||
});
|
||||
|
||||
it('returns false is user is not a leader or admin', () => {
|
||||
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('returns false is user is an admin but group is a party', () => {
|
||||
guild.type = 'party';
|
||||
user.contributor.admin = true;
|
||||
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('editGroup', () => {
|
||||
var guild;
|
||||
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
describe('Group Tasks Meta Actions Controller', () => {
|
||||
let rootScope, scope, user, userSerivce;
|
||||
|
||||
beforeEach(() => {
|
||||
module(function($provide) {
|
||||
$provide.value('User', {});
|
||||
});
|
||||
|
||||
inject(($rootScope, $controller) => {
|
||||
rootScope = $rootScope;
|
||||
|
||||
user = specHelper.newUser();
|
||||
user._id = "unique-user-id";
|
||||
userSerivce = {user: user};
|
||||
|
||||
scope = $rootScope.$new();
|
||||
|
||||
scope.task = {
|
||||
group: {
|
||||
assignedUsers: [],
|
||||
approval: {
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
scope.task._edit = angular.copy(scope.task);
|
||||
|
||||
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleTaskRequiresApproval', function () {
|
||||
it('toggles task approval required field from false to true', function () {
|
||||
scope.toggleTaskRequiresApproval();
|
||||
expect(scope.task._edit.group.approval.required).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles task approval required field from true to false', function () {
|
||||
scope.task._edit.group.approval.required = true;
|
||||
scope.toggleTaskRequiresApproval();
|
||||
expect(scope.task._edit.group.approval.required).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assign events', function () {
|
||||
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
|
||||
var testId = 'test-id';
|
||||
rootScope.$broadcast('addedGroupMember', testId);
|
||||
expect(scope.task.group.assignedUsers).to.contain(testId);
|
||||
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
|
||||
});
|
||||
|
||||
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
|
||||
var testId = 'test-id';
|
||||
scope.task.group.assignedUsers.push(testId);
|
||||
scope.task._edit.group.assignedUsers.push(testId);
|
||||
rootScope.$broadcast('removedGroupMember', testId);
|
||||
expect(scope.task.group.assignedUsers).to.not.contain(testId);
|
||||
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Group Tasks Meta Actions Controller', () => {
|
||||
let rootScope, scope, user, userSerivce;
|
||||
|
||||
beforeEach(() => {
|
||||
module(function($provide) {
|
||||
$provide.value('User', {});
|
||||
});
|
||||
|
||||
inject(($rootScope, $controller) => {
|
||||
rootScope = $rootScope;
|
||||
|
||||
user = specHelper.newUser();
|
||||
user._id = "unique-user-id";
|
||||
userSerivce = {user: user};
|
||||
|
||||
scope = $rootScope.$new();
|
||||
|
||||
scope.task = {
|
||||
group: {
|
||||
assignedUsers: [],
|
||||
approval: {
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
scope.task._edit = angular.copy(scope.task);
|
||||
|
||||
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleTaskRequiresApproval', function () {
|
||||
it('toggles task approval required field from false to true', function () {
|
||||
scope.toggleTaskRequiresApproval();
|
||||
expect(scope.task._edit.group.approval.required).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles task approval required field from true to false', function () {
|
||||
scope.task._edit.group.approval.required = true;
|
||||
scope.toggleTaskRequiresApproval();
|
||||
expect(scope.task._edit.group.approval.required).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('assign events', function () {
|
||||
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
|
||||
var testId = 'test-id';
|
||||
rootScope.$broadcast('addedGroupMember', testId);
|
||||
expect(scope.task.group.assignedUsers).to.contain(testId);
|
||||
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
|
||||
});
|
||||
|
||||
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
|
||||
var testId = 'test-id';
|
||||
scope.task.group.assignedUsers.push(testId);
|
||||
scope.task._edit.group.assignedUsers.push(testId);
|
||||
rootScope.$broadcast('removedGroupMember', testId);
|
||||
expect(scope.task.group.assignedUsers).to.not.contain(testId);
|
||||
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ describe('Notification Controller', function() {
|
||||
beforeEach(function() {
|
||||
user = specHelper.newUser();
|
||||
user._id = "unique-user-id";
|
||||
user.needsCron = false;
|
||||
|
||||
var userSync = sinon.stub().returns({
|
||||
then: function then (f) { f(); }
|
||||
|
||||
@@ -334,4 +334,34 @@ describe('Settings Controller', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Fixing character values', function () {
|
||||
describe('#restore', function () {
|
||||
var blankRestoreValues = {
|
||||
stats: {
|
||||
hp: 0,
|
||||
exp: 0,
|
||||
gp: 0,
|
||||
lvl: 0,
|
||||
mp: 0,
|
||||
},
|
||||
achievements: {
|
||||
streak: 0,
|
||||
},
|
||||
};
|
||||
|
||||
it('doesn\'t update character values when level is less than 1', function () {
|
||||
scope.restoreValues = blankRestoreValues;
|
||||
scope.restore();
|
||||
expect(User.set).to.not.be.called;
|
||||
});
|
||||
|
||||
it('updates character values when level is at least 1', function () {
|
||||
scope.restoreValues = blankRestoreValues;
|
||||
scope.restoreValues.stats.lvl = 1;
|
||||
scope.restore();
|
||||
expect(User.set).to.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
describe('closeMenu Directive', function() {
|
||||
var scope;
|
||||
|
||||
beforeEach(module('habitrpg'));
|
||||
|
||||
beforeEach(inject(function($rootScope) {
|
||||
scope = $rootScope.$new();
|
||||
|
||||
scope.$digest();
|
||||
}));
|
||||
|
||||
it('closes a connected menu when element is clicked', inject(function($compile) {
|
||||
var menuElement = $compile('<a data-close-menu menu="mobile">')(scope);
|
||||
scope._expandedMenu = { menu: 'mobile' };
|
||||
|
||||
menuElement.appendTo(document.body);
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
}));
|
||||
|
||||
it('closes a connected menu when child element is clicked', inject(function($compile) {
|
||||
var menuElementWithChild = $compile('<li></li>')(scope);
|
||||
var menuElementChild = $compile('<a data-close-menu></a>')(scope);
|
||||
scope._expandedMenu = { menu: 'mobile' };
|
||||
|
||||
menuElementWithChild.appendTo(document.body);
|
||||
menuElementChild.appendTo(menuElementWithChild);
|
||||
menuElementChild.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
}));
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
describe('expandMenu Directive', function() {
|
||||
var menuElement, scope;
|
||||
|
||||
beforeEach(module('habitrpg'));
|
||||
|
||||
beforeEach(inject(function($rootScope, $compile) {
|
||||
scope = $rootScope.$new();
|
||||
|
||||
var element = '<a data-expand-menu menu="mobile"></a>';
|
||||
|
||||
menuElement = $compile(element)(scope);
|
||||
scope.$digest();
|
||||
}));
|
||||
|
||||
it('expands a connected menu when element is clicked', function() {
|
||||
expect(scope._expandedMenu).to.not.exist;
|
||||
menuElement.appendTo(document.body);
|
||||
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql('mobile')
|
||||
});
|
||||
|
||||
it('closes a connected menu when it is already open', function() {
|
||||
scope._expandedMenu = {};
|
||||
scope._expandedMenu.menu = 'mobile';
|
||||
menuElement.appendTo(document.body);
|
||||
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
});
|
||||
});
|
||||
@@ -206,6 +206,20 @@ describe('Quests Service', function() {
|
||||
});
|
||||
});
|
||||
|
||||
context('quest bundles', function() {
|
||||
it('sends bundle object', function(done) {
|
||||
questsService.buyQuest('featheredFriends')
|
||||
.then(function(res) {
|
||||
expect(res).to.eql(content.bundles.featheredFriends);
|
||||
expect(window.alert).to.not.be.called;
|
||||
expect(rejectSpy).to.not.be.called;
|
||||
done();
|
||||
}, rejectSpy);
|
||||
|
||||
scope.$apply();
|
||||
});
|
||||
});
|
||||
|
||||
context('all other quests', function() {
|
||||
it('sends quest object', function(done) {
|
||||
questsService.buyQuest('whale')
|
||||
|
||||
@@ -2,5 +2,10 @@
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"habitrpg/browser",
|
||||
"habitrpg/mocha",
|
||||
"habitrpg/esnext",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue';
|
||||
import DrawerComponent from 'client/components/inventory/drawer.vue';
|
||||
|
||||
describe('DrawerComponent', () => {
|
||||
it('sets the correct default data', () => {
|
||||
expect(DrawerComponent.data).to.be.a('function');
|
||||
const defaultData = DrawerComponent.data();
|
||||
expect(defaultData.open).to.be.true;
|
||||
});
|
||||
|
||||
it('renders the correct title', () => {
|
||||
const Ctor = Vue.extend(DrawerComponent);
|
||||
const vm = new Ctor({propsData: {
|
||||
title: 'My title',
|
||||
}}).$mount();
|
||||
|
||||
expect(vm.$el.textContent).to.be.equal('My title');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import roundBigNumberFilter from 'client/filters/roundBigNumber';
|
||||
|
||||
describe('round big number filter', () => {
|
||||
it('can round a decimal number', () => {
|
||||
expect(roundBigNumberFilter(4.567)).to.equal(4.57);
|
||||
expect(roundBigNumberFilter(4.562)).to.equal(4.56);
|
||||
});
|
||||
|
||||
it('can round thousands', () => {
|
||||
expect(roundBigNumberFilter(70065)).to.equal('70.1k');
|
||||
});
|
||||
|
||||
it('can round milions', () => {
|
||||
expect(roundBigNumberFilter(10000987)).to.equal('10.0m');
|
||||
});
|
||||
|
||||
it('can round bilions', () => {
|
||||
expect(roundBigNumberFilter(1000000000)).to.equal('1.0b');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import groupsUtilities from 'client/mixins/groupsUtilities';
|
||||
import { TAVERN_ID } from 'common/script/constants';
|
||||
import generateStore from 'client/store';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('Groups Utilities Mixin', () => {
|
||||
@@ -7,6 +8,7 @@ describe('Groups Utilities Mixin', () => {
|
||||
|
||||
before(() => {
|
||||
instance = new Vue({
|
||||
store: generateStore(),
|
||||
mixins: [groupsUtilities],
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('tasks actions', () => {
|
||||
});
|
||||
|
||||
describe('fetchUserTasks', () => {
|
||||
it('fetches user tasks', async () => {
|
||||
xit('fetches user tasks', async () => {
|
||||
expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED');
|
||||
const tasks = [{_id: 1}];
|
||||
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||
@@ -36,7 +36,7 @@ describe('tasks actions', () => {
|
||||
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||
});
|
||||
|
||||
it('can reload tasks if forceLoad is true', async () => {
|
||||
xit('can reload tasks if forceLoad is true', async () => {
|
||||
store.state.tasks = {
|
||||
loadingStatus: 'LOADED',
|
||||
data: [{_id: 1}],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import generateStore from 'client/store';
|
||||
|
||||
describe('tasks actions', () => {
|
||||
describe('user actions', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -121,6 +121,17 @@ describe('achievements', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('card achievements exist with counts', () => {
|
||||
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell', 'goodluck'];
|
||||
cardTypes.forEach((card) => {
|
||||
let cardAchiev = basicAchievs[`${card}Cards`];
|
||||
|
||||
expect(cardAchiev).to.exist;
|
||||
expect(cardAchiev).to.have.property('optionalCount')
|
||||
.that.is.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('rebirth achievement exists with no count', () => {
|
||||
let rebirth = basicAchievs.rebirth;
|
||||
|
||||
@@ -174,7 +185,7 @@ describe('achievements', () => {
|
||||
});
|
||||
|
||||
it('card achievements exist with counts', () => {
|
||||
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
|
||||
let cardTypes = ['nye', 'valentine'];
|
||||
cardTypes.forEach((card) => {
|
||||
let cardAchiev = seasonalAchievs[`${card}Cards`];
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ describe('shops', () => {
|
||||
expect(shopCategories.length).to.be.greaterThan(2);
|
||||
});
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
let identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
@@ -47,11 +53,19 @@ describe('shops', () => {
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
_.each(category.items, (item) => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
if (category.identifier === 'bundle') {
|
||||
_.each(category.items, (item) => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
_.each(category.items, (item) => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import forEach from 'lodash/forEach';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('shared.ops.purchase', () => {
|
||||
const SEASONAL_FOOD = 'Meat';
|
||||
@@ -200,5 +202,28 @@ describe('shared.ops.purchase', () => {
|
||||
|
||||
expect(user.items.gear.owned[key]).to.be.true;
|
||||
});
|
||||
|
||||
it('purchases quest bundles', () => {
|
||||
let startingBalance = user.balance;
|
||||
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf());
|
||||
let type = 'bundles';
|
||||
let key = 'featheredFriends';
|
||||
let price = 1.75;
|
||||
let questList = [
|
||||
'falcon',
|
||||
'harpy',
|
||||
'owl',
|
||||
];
|
||||
|
||||
purchase(user, {params: {type, key}});
|
||||
|
||||
forEach(questList, (bundledKey) => {
|
||||
expect(user.items.quests[bundledKey]).to.equal(1);
|
||||
});
|
||||
|
||||
expect(user.balance).to.equal(startingBalance - price);
|
||||
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1049
-217
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
|
||||
## Babel Paths for Production Environment
|
||||
|
||||
In development, we [transpile at server start](https://github.com/HabitRPG/habitrpg/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
|
||||
In development, we [transpile at server start](https://github.com/HabitRPG/habitica/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
|
||||
|
||||
This system means that requiring any files from `common/script` in `website/server/**/*.js` must be done through the `common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
|
||||
This system means that requiring any files from `website/common/script` in `website/server/**/*.js` must be done through the `website/common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
|
||||
|
||||
This test just verifies that none of the files in the server code are calling the common files directly.
|
||||
|
||||
Executable → Regular
+2
-2
@@ -16,7 +16,7 @@ nvm use
|
||||
nvm alias default current
|
||||
|
||||
echo Update npm...
|
||||
npm install -g npm@3
|
||||
npm install -g npm@4
|
||||
|
||||
echo Installing global modules...
|
||||
npm install -g gulp bower grunt-cli mocha
|
||||
npm install -g gulp bower grunt-cli mocha node-pre-gyp
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = {
|
||||
index: path.resolve(__dirname, '../../dist-client/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../../dist-client'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/new-app',
|
||||
assetsPublicPath: '/new-app/',
|
||||
staticAssetsDirectory,
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
|
||||
@@ -17,21 +17,31 @@ const baseConfig = {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: IS_PROD ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js',
|
||||
devtoolModuleFilenameTemplate (info) {
|
||||
// Fix source maps, code from
|
||||
// https://github.com/Darkside73/bbsmile.com.ua/commit/3596d3c42ef91b69d8380359c3e8908edc08acdb
|
||||
let filename = info.resourcePath;
|
||||
if (info.resource.match(/\.vue$/) && !info.allLoaders.match(/type=script/)) {
|
||||
filename = 'generated';
|
||||
}
|
||||
|
||||
return filename;
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue', '.json'],
|
||||
modules: [
|
||||
path.join(__dirname, '..', 'website'),
|
||||
path.join(__dirname, '..', 'test/client/unit'),
|
||||
path.join(__dirname, '..', 'node_modules'),
|
||||
path.join(projectRoot, 'website'),
|
||||
path.join(projectRoot, 'test/client/unit'),
|
||||
path.join(projectRoot, 'node_modules'),
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery',
|
||||
website: path.resolve(__dirname, '../website'),
|
||||
common: path.resolve(__dirname, '../website/common'),
|
||||
client: path.resolve(__dirname, '../website/client'),
|
||||
assets: path.resolve(__dirname, '../website/client/assets'),
|
||||
components: path.resolve(__dirname, '../website/client/components'),
|
||||
website: path.resolve(projectRoot, 'website'),
|
||||
common: path.resolve(projectRoot, 'website/common'),
|
||||
client: path.resolve(projectRoot, 'website/client'),
|
||||
assets: path.resolve(projectRoot, 'website/client/assets'),
|
||||
components: path.resolve(projectRoot, 'website/client/components'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -63,11 +73,17 @@ const baseConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
include: [
|
||||
path.join(projectRoot, 'test'),
|
||||
path.join(projectRoot, 'website'),
|
||||
path.join(projectRoot, 'node_modules', 'bootstrap-vue'),
|
||||
],
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
test: /\.(png|jpe?g|gif)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
@@ -82,6 +98,22 @@ const baseConfig = {
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{ loader: 'svg-inline-loader' },
|
||||
{ loader: 'svgo-loader' },
|
||||
],
|
||||
exclude: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{ loader: 'svg-url-loader' },
|
||||
{ loader: 'svgo-loader' },
|
||||
],
|
||||
include: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user