mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-30 04:32:45 +01:00
Compare commits
310 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17fb681671 | ||
|
|
0069aee5b0 | ||
|
|
240dd1b965 | ||
|
|
88e6b2da7c | ||
|
|
2cf8439bd1 | ||
|
|
0e404ad6ba | ||
|
|
b9f709ab30 | ||
|
|
d57c525fab | ||
|
|
22a12e37fa | ||
|
|
5220cc1bf3 | ||
|
|
3b4b459e68 | ||
|
|
bbbdd89ade | ||
|
|
a20c1ba751 | ||
|
|
545b052c10 | ||
|
|
85b861c4a9 | ||
|
|
762e87a82a | ||
|
|
b68e69e1a1 | ||
|
|
4764f115b1 | ||
|
|
2da2a47f32 | ||
|
|
8f744565e2 | ||
|
|
714512b0a3 | ||
|
|
9538c86d02 | ||
|
|
6988875e8a | ||
|
|
53bbd93d80 | ||
|
|
75092336c4 | ||
|
|
310bdf8cb5 | ||
|
|
9435a3089a | ||
|
|
bb6dac2e84 | ||
|
|
acf34e2344 | ||
|
|
1aac4c713d | ||
|
|
bb527caa06 | ||
|
|
98bb6fd7ce | ||
|
|
b8c716ff82 | ||
|
|
9830fce760 | ||
|
|
7fccf59f50 | ||
|
|
dd79f2be60 | ||
|
|
fbdcd4b0a3 | ||
|
|
e229bc5042 | ||
|
|
44c7e8c9dc | ||
|
|
c4ffe39ec9 | ||
|
|
dc3d694d0e | ||
|
|
4f0ce77205 | ||
|
|
c28ec24c33 | ||
|
|
54db84fddc | ||
|
|
e7fd2b4c79 | ||
|
|
05640f513e | ||
|
|
b0ebdfeb65 | ||
|
|
6c01db8d81 | ||
|
|
5a3751cbac | ||
|
|
7802e30e80 | ||
|
|
899452279b | ||
|
|
566716e2fe | ||
|
|
2a42bc9450 | ||
|
|
1ef62d1b66 | ||
|
|
355773ecf3 | ||
|
|
2bb5751f33 | ||
|
|
2570c59130 | ||
|
|
2dfcda068b | ||
|
|
507133c76e | ||
|
|
a7c115877f | ||
|
|
1750a0c2e6 | ||
|
|
759ce61492 | ||
|
|
57193bd5f3 | ||
|
|
e1a1b4eab6 | ||
|
|
350894f985 | ||
|
|
0184d774c2 | ||
|
|
d136162d48 | ||
|
|
2be8ddb60d | ||
|
|
3c67f91525 | ||
|
|
c02aadfac4 | ||
|
|
2f956252ab | ||
|
|
341f16cc82 | ||
|
|
ec179182e7 | ||
|
|
b886d7bb33 | ||
|
|
a8f8f4f544 | ||
|
|
4047bf6943 | ||
|
|
a5a985fd00 | ||
|
|
444d6889de | ||
|
|
c56c69d464 | ||
|
|
4b610ba3f1 | ||
|
|
65e3b599e6 | ||
|
|
7caf211bec | ||
|
|
d4bc7c77a9 | ||
|
|
bfaa7c0fea | ||
|
|
8367de34bf | ||
|
|
ea6b78b7ca | ||
|
|
401067bfed | ||
|
|
b457daa616 | ||
|
|
54c2441934 | ||
|
|
9e8807c40d | ||
|
|
9bfbeaf93e | ||
|
|
860efefdb2 | ||
|
|
6310482b9d | ||
|
|
2d4928cd2b | ||
|
|
f3c2c0f901 | ||
|
|
1fc84c2357 | ||
|
|
13cdcedcba | ||
|
|
72f0b8ed7c | ||
|
|
95f9479d7a | ||
|
|
af095d8450 | ||
|
|
470495387c | ||
|
|
bdef1ca23c | ||
|
|
1835804e86 | ||
|
|
cb58994bdf | ||
|
|
44f3b73183 | ||
|
|
7e23fdc22a | ||
|
|
e138d2b67b | ||
|
|
3e3248fecb | ||
|
|
78ee60611a | ||
|
|
3c7aaa605b | ||
|
|
00343da266 | ||
|
|
56d09411d9 | ||
|
|
ae0df2242a | ||
|
|
5b06b28c97 | ||
|
|
c6a3bfb291 | ||
|
|
7797794cd5 | ||
|
|
d9e09a5f3d | ||
|
|
4e73c8513e | ||
|
|
9421fd7ced | ||
|
|
699de64328 | ||
|
|
6f9cbf9ca1 | ||
|
|
a097819b72 | ||
|
|
77f71b5415 | ||
|
|
ced3621dea | ||
|
|
e321d85b3c | ||
|
|
d72b40d5b0 | ||
|
|
54443a2980 | ||
|
|
00dc990974 | ||
|
|
3737aa045d | ||
|
|
b03ddf6f7d | ||
|
|
4ab89fd3e0 | ||
|
|
f1e200c0f5 | ||
|
|
218664dfcc | ||
|
|
a0f29e970d | ||
|
|
200cd66d66 | ||
|
|
dd05a8d608 | ||
|
|
299e88233c | ||
|
|
26bde1f766 | ||
|
|
d95836b881 | ||
|
|
fac81bb9ee | ||
|
|
b323abd225 | ||
|
|
b3870e5f34 | ||
|
|
29dc56c12f | ||
|
|
b62f08d500 | ||
|
|
f62177fb1a | ||
|
|
885f2998ae | ||
|
|
2afd96e11c | ||
|
|
cd92f44365 | ||
|
|
863177902a | ||
|
|
96974461e5 | ||
|
|
8895b70ffa | ||
|
|
03480ebfc7 | ||
|
|
9b8676f02e | ||
|
|
3e7738b5b1 | ||
|
|
33a235b46c | ||
|
|
137d6c1f9d | ||
|
|
1a5e820d88 | ||
|
|
0c7f9ca6bb | ||
|
|
3e6b3ce3ff | ||
|
|
ea5ba965e7 | ||
|
|
7215a550b5 | ||
|
|
3235dfa236 | ||
|
|
9baf7a7c67 | ||
|
|
cd629ef7fa | ||
|
|
9ef7c45241 | ||
|
|
fef3d09f2d | ||
|
|
53c83c585a | ||
|
|
e628c5dc3b | ||
|
|
9eaa531f66 | ||
|
|
3ffea4332e | ||
|
|
4618fd8954 | ||
|
|
791c19b5f1 | ||
|
|
7193cc6bae | ||
|
|
1845bd1e35 | ||
|
|
5f468d16b7 | ||
|
|
20a99e526d | ||
|
|
1e69f42d0f | ||
|
|
9c2f5213cb | ||
|
|
c06d5107ac | ||
|
|
1eb0f5baa5 | ||
|
|
b28189fff5 | ||
|
|
82497e4041 | ||
|
|
2a5e9c0780 | ||
|
|
c8ca67aa64 | ||
|
|
89e4cbcffe | ||
|
|
67564317fb | ||
|
|
dc2269a307 | ||
|
|
0c713ab368 | ||
|
|
be5d776fb4 | ||
|
|
b0051c45b4 | ||
|
|
b82044e07b | ||
|
|
b4c4769208 | ||
|
|
42a05446c0 | ||
|
|
9f7c0b4861 | ||
|
|
4814b0c52b | ||
|
|
67b16d91a3 | ||
|
|
8d444980de | ||
|
|
08ccd595f2 | ||
|
|
6b625a60ab | ||
|
|
6bdb695616 | ||
|
|
b999d46142 | ||
|
|
ce0f5af08d | ||
|
|
c5296d4cb0 | ||
|
|
22b683b1d9 | ||
|
|
229fd06ee3 | ||
|
|
eb8f84aae0 | ||
|
|
69b69e9d27 | ||
|
|
8033e7c0a0 | ||
|
|
e39b80bb9a | ||
|
|
2038d5e7c8 | ||
|
|
46a8ee52d4 | ||
|
|
52064f6b2a | ||
|
|
f15a27a7f1 | ||
|
|
fcf0dd87f9 | ||
|
|
ab974675b9 | ||
|
|
cb612d99d7 | ||
|
|
cb5a47ec7b | ||
|
|
838b9a5822 | ||
|
|
6c65056e2b | ||
|
|
2bf0fdf4a2 | ||
|
|
cd5ff04ee4 | ||
|
|
dbc5b9f850 | ||
|
|
ca3437d676 | ||
|
|
c43ca62bc4 | ||
|
|
eaa91b2a09 | ||
|
|
6259b68b4f | ||
|
|
08073acf11 | ||
|
|
bddafd4392 | ||
|
|
3ab14e4e5a | ||
|
|
d9830950aa | ||
|
|
45696a6273 | ||
|
|
3f92317b9e | ||
|
|
848883736d | ||
|
|
69e0ab11c0 | ||
|
|
d8d7a81edf | ||
|
|
03a09b7546 | ||
|
|
0d2737572d | ||
|
|
28149202db | ||
|
|
4b23cd9f23 | ||
|
|
8aaabdc086 | ||
|
|
6e6ca05352 | ||
|
|
74d8ecc732 | ||
|
|
63c8a09e22 | ||
|
|
865f623c99 | ||
|
|
061968dd1a | ||
|
|
21bc91c3ae | ||
|
|
0bfc6608c1 | ||
|
|
4108a22d78 | ||
|
|
34f6b63968 | ||
|
|
8d1ebff7e9 | ||
|
|
993df72708 | ||
|
|
50d3226a86 | ||
|
|
17ce2febf9 | ||
|
|
0caa195c6f | ||
|
|
f964e3c0a5 | ||
|
|
9d69d4b863 | ||
|
|
19500600bc | ||
|
|
f25fe9e263 | ||
|
|
5f37487c23 | ||
|
|
12fd79059b | ||
|
|
232061a629 | ||
|
|
3fcc1c522d | ||
|
|
8302c50302 | ||
|
|
6eb06fb054 | ||
|
|
286c8c7530 | ||
|
|
47ab8f2073 | ||
|
|
83353f6481 | ||
|
|
9cbd7ad62d | ||
|
|
3485a1d0bc | ||
|
|
2e5106fda1 | ||
|
|
2e5f5714e4 | ||
|
|
3cf7b2c96c | ||
|
|
286db39478 | ||
|
|
4d4c1cfaf3 | ||
|
|
d7ad3efabf | ||
|
|
f8876fe055 | ||
|
|
b973335d69 | ||
|
|
3b6fce0708 | ||
|
|
ff6bd6de71 | ||
|
|
042afe1df3 | ||
|
|
a208ba4aba | ||
|
|
0e958fd306 | ||
|
|
d98fe79e9c | ||
|
|
0e5a811b98 | ||
|
|
a28aea65f8 | ||
|
|
0f92349902 | ||
|
|
d4881cb73a | ||
|
|
b3216fdb85 | ||
|
|
3e37941e0a | ||
|
|
32088767ac | ||
|
|
f4d021ab8c | ||
|
|
8532203717 | ||
|
|
365daba6fc | ||
|
|
70692752c7 | ||
|
|
95d4016678 | ||
|
|
061457b268 | ||
|
|
e7ec9a6d65 | ||
|
|
d1396e7bc6 | ||
|
|
d5cedaa925 | ||
|
|
bea813b318 | ||
|
|
b31268fbc2 | ||
|
|
35727228f0 | ||
|
|
feb7ab8345 | ||
|
|
bba2e71af3 | ||
|
|
26123ac6ae | ||
|
|
addee73e4d | ||
|
|
9736ef0d25 | ||
|
|
638259b885 | ||
|
|
d2f0d7b20b | ||
|
|
3c9f7ff9d8 |
@@ -6,6 +6,9 @@ website/transpiled-babel/
|
||||
website/common/transpiled-babel/
|
||||
dist/
|
||||
dist-client/
|
||||
apidoc_build/
|
||||
content_cache/
|
||||
node_modules/
|
||||
|
||||
# Not linted
|
||||
website/client-old/
|
||||
@@ -16,5 +19,3 @@ migrations/*
|
||||
scripts/*
|
||||
website/common/browserify.js
|
||||
Gruntfile.js
|
||||
gulpfile.js
|
||||
gulp
|
||||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
# Pull Request
|
||||
|
||||
[Please see these instructions for adding a pull request](http://habitica.wikia.com/wiki/Using_Habitica_Git#Pull_Request)
|
||||
[Please see these instructions for adding a pull request](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API)
|
||||
|
||||
# Requesting a feature
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,7 +6,7 @@
|
||||
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
||||
[//]: # (Fill out relevant information - UUID is found from the Habitia website at User Icon > Settings > API)
|
||||
### General Info
|
||||
* UUID:
|
||||
* Browser:
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Habitica_Git#Pull_Request for more info)
|
||||
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info)
|
||||
|
||||
[//]: # (Put Issue # or URL here, if applicable. This will automatically close the issue if your PR is merged in)
|
||||
Fixes put_issue_url_here
|
||||
@@ -8,7 +8,7 @@ Fixes put_issue_url_here
|
||||
|
||||
|
||||
|
||||
[//]: # (Put User ID in here - found in Settings -> API)
|
||||
[//]: # (Put User ID in here - found on the Habitica website at User Icon > Settings > API)
|
||||
|
||||
----
|
||||
UUID:
|
||||
|
||||
@@ -34,5 +34,4 @@ env:
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
- TEST="apidoc"
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
FROM node:boron
|
||||
|
||||
ENV ADMIN_EMAIL admin@habitica.com
|
||||
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
|
||||
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
|
||||
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
|
||||
ENV BASE_URL https://habitica.com
|
||||
ENV FACEBOOK_KEY 128307497299777
|
||||
ENV GA_ID UA-33510635-1
|
||||
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
|
||||
ENV NODE_ENV production
|
||||
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
|
||||
|
||||
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
|
||||
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
|
||||
RUN yarn global add npm@5
|
||||
@@ -9,7 +20,7 @@ RUN npm install -g gulp mocha
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone --branch v4.0.3 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN git clone --branch v4.16.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN gulp build:prod --force
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
},
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"LOGGLY_TOKEN": "token",
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_ACCOUNT": "account",
|
||||
"PUSH_CONFIGS": {
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true,
|
||||
},
|
||||
"extends": [
|
||||
"habitrpg/server",
|
||||
"habitrpg/babel"
|
||||
],
|
||||
}
|
||||
@@ -22,5 +22,5 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
});
|
||||
|
||||
gulp.task('apidoc:watch', ['apidoc'], () => {
|
||||
return gulp.watch(APIDOC_SRC_PATH + '/**/*.js', ['apidoc']);
|
||||
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, ['apidoc']);
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
@@ -1,10 +1,9 @@
|
||||
import gulp from 'gulp';
|
||||
import runSequence from 'run-sequence';
|
||||
import babel from 'gulp-babel';
|
||||
import webpackProductionBuild from '../webpack/build';
|
||||
|
||||
gulp.task('build', () => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
gulp.start('build:prod');
|
||||
}
|
||||
});
|
||||
@@ -24,15 +23,15 @@ gulp.task('build:common', () => {
|
||||
gulp.task('build:server', ['build:src', 'build:common']);
|
||||
|
||||
// Client Production Build
|
||||
gulp.task('build:client', ['bootstrap'], (done) => {
|
||||
gulp.task('build:client', (done) => {
|
||||
webpackProductionBuild((err, output) => {
|
||||
if (err) return done(err);
|
||||
console.log(output);
|
||||
console.log(output); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build:prod', [
|
||||
'build:server',
|
||||
'build:server',
|
||||
'build:client',
|
||||
'apidoc',
|
||||
]);
|
||||
|
||||
@@ -7,10 +7,11 @@ import gulp from 'gulp';
|
||||
|
||||
// Add additional properties to the repl's context
|
||||
let improveRepl = (context) => {
|
||||
|
||||
// Let "exit" and "quit" terminate the console
|
||||
['exit', 'quit'].forEach((term) => {
|
||||
Object.defineProperty(context, term, { get () { process.exit(); }});
|
||||
Object.defineProperty(context, term, { get () {
|
||||
process.exit();
|
||||
}});
|
||||
});
|
||||
|
||||
// "clear" clears the screen
|
||||
@@ -18,12 +19,12 @@ let improveRepl = (context) => {
|
||||
process.stdout.write('\u001B[2J\u001B[0;0f');
|
||||
}});
|
||||
|
||||
context.Challenge = require('../website/server/models/challenge').model;
|
||||
context.Group = require('../website/server/models/group').model;
|
||||
context.User = require('../website/server/models/user').model;
|
||||
context.Challenge = require('../website/server/models/challenge').model; // eslint-disable-line global-require
|
||||
context.Group = require('../website/server/models/group').model; // eslint-disable-line global-require
|
||||
context.User = require('../website/server/models/user').model; // eslint-disable-line global-require
|
||||
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var mongooseOptions = !isProd ? {} : {
|
||||
const isProd = nconf.get('NODE_ENV') === 'production';
|
||||
const mongooseOptions = !isProd ? {} : {
|
||||
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
};
|
||||
@@ -31,16 +32,15 @@ let improveRepl = (context) => {
|
||||
mongoose.connect(
|
||||
nconf.get('NODE_DB_URI'),
|
||||
mongooseOptions,
|
||||
function (err) {
|
||||
(err) => {
|
||||
if (err) throw err;
|
||||
logger.info('Connected with Mongoose');
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
gulp.task('console', (cb) => {
|
||||
gulp.task('console', () => {
|
||||
improveRepl(repl.start({
|
||||
prompt: 'Habitica > ',
|
||||
}).context);
|
||||
|
||||
@@ -11,78 +11,38 @@ import {each} from 'lodash';
|
||||
// https://github.com/Ensighten/grunt-spritesmith/issues/67#issuecomment-34786248
|
||||
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
|
||||
|
||||
const IMG_DIST_PATH = 'website/static/sprites/';
|
||||
const IMG_DIST_PATH = 'website/client/assets/images/sprites/';
|
||||
const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
function calculateImgDimensions (img, addPadding) {
|
||||
let dims = sizeOf(img);
|
||||
|
||||
gulp.task('sprites:largeSprites', () => {
|
||||
let largeSrc = sync('website/raw_sprites/spritesmith_large/**/*.png');
|
||||
return createSpritesStream('largeSprites', largeSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', (done) => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
console.log('Verifiying that images do not exceed max dimensions');
|
||||
|
||||
let numberOfSheetsThatAreTooBig = 0;
|
||||
|
||||
let distSpritesheets = sync(`${IMG_DIST_PATH}*.png`);
|
||||
|
||||
each(distSpritesheets, (img, index) => {
|
||||
let spriteSize = calculateImgDimensions(img);
|
||||
|
||||
if (spriteSize > MAX_SPRITESHEET_SIZE) {
|
||||
numberOfSheetsThatAreTooBig++;
|
||||
let name = basename(img, '.png');
|
||||
console.error(`WARNING: ${name} might be too big - ${spriteSize} > ${MAX_SPRITESHEET_SIZE}`);
|
||||
}
|
||||
});
|
||||
|
||||
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/habitica/pull/6683#issuecomment-185462180
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions');
|
||||
let requiresSpecialTreatment = checkForSpecialTreatment(img);
|
||||
if (requiresSpecialTreatment) {
|
||||
let newWidth = dims.width < 90 ? 90 : dims.width;
|
||||
let newHeight = dims.height < 90 ? 90 : dims.height;
|
||||
dims = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
let spritesheetSliceIndicies = calculateSpritesheetsSrcIndicies(src);
|
||||
let stream = mergeStream();
|
||||
let padding = 0;
|
||||
|
||||
each(spritesheetSliceIndicies, (start, index) => {
|
||||
let slicedSrc = src.slice(start, spritesheetSliceIndicies[index + 1]);
|
||||
if (addPadding) {
|
||||
padding = dims.width * 8 + dims.height * 8;
|
||||
}
|
||||
|
||||
let spriteData = gulp.src(slicedSrc)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}-${index}.png`,
|
||||
cssName: `spritesmith-${name}-${index}.css`,
|
||||
algorithm: 'binary-tree',
|
||||
padding: 1,
|
||||
cssTemplate: 'website/raw_sprites/css/css.template.handlebars',
|
||||
cssVarMap: cssVarMap,
|
||||
}));
|
||||
if (!dims.width || !dims.height) console.error('MISSING DIMENSIONS:', dims); // eslint-disable-line no-console
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH));
|
||||
let totalPixelSize = dims.width * dims.height + padding;
|
||||
|
||||
let cssStream = spriteData.css
|
||||
.pipe(gulp.dest(CSS_DIST_PATH));
|
||||
|
||||
stream.add(imgStream);
|
||||
stream.add(cssStream);
|
||||
});
|
||||
|
||||
return stream;
|
||||
return totalPixelSize;
|
||||
}
|
||||
|
||||
function calculateSpritesheetsSrcIndicies (src) {
|
||||
@@ -102,37 +62,6 @@ function calculateSpritesheetsSrcIndicies (src) {
|
||||
return slices;
|
||||
}
|
||||
|
||||
function calculateImgDimensions (img, addPadding) {
|
||||
let dims = sizeOf(img);
|
||||
|
||||
let requiresSpecialTreatment = checkForSpecialTreatment(img);
|
||||
if (requiresSpecialTreatment) {
|
||||
let newWidth = dims.width < 90 ? 90 : dims.width;
|
||||
let newHeight = dims.height < 90 ? 90 : dims.height;
|
||||
dims = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
}
|
||||
|
||||
let padding = 0;
|
||||
|
||||
if (addPadding) {
|
||||
padding = (dims.width * 8) + (dims.height * 8);
|
||||
}
|
||||
|
||||
if (!dims.width || !dims.height) console.error('MISSING DIMENSIONS:', dims);
|
||||
|
||||
let totalPixelSize = (dims.width * dims.height) + padding;
|
||||
|
||||
return totalPixelSize;
|
||||
}
|
||||
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
function cssVarMap (sprite) {
|
||||
// For hair, skins, beards, etc. we want to output a '.customize-options.WHATEVER' class, which works as a
|
||||
// 60x60 image pointing at the proper part of the 90x90 sprite.
|
||||
@@ -141,18 +70,93 @@ function cssVarMap (sprite) {
|
||||
if (requiresSpecialTreatment) {
|
||||
sprite.custom = {
|
||||
px: {
|
||||
offset_x: `-${ sprite.x + 25 }px`,
|
||||
offset_y: `-${ sprite.y + 15 }px`,
|
||||
offsetX: `-${ sprite.x + 25 }px`,
|
||||
offsetY: `-${ sprite.y + 15 }px`,
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (~sprite.name.indexOf('shirt'))
|
||||
sprite.custom.px.offset_y = `-${ sprite.y + 30 }px`; // even more for shirts
|
||||
if (~sprite.name.indexOf('hair_base')) {
|
||||
let styleArray = sprite.name.split('_').slice(2,3);
|
||||
if (sprite.name.indexOf('shirt') !== -1)
|
||||
sprite.custom.px.offsetY = `-${ sprite.y + 35 }px`; // even more for shirts
|
||||
if (sprite.name.indexOf('hair_base') !== -1) {
|
||||
let styleArray = sprite.name.split('_').slice(2, 3);
|
||||
if (Number(styleArray[0]) > 14)
|
||||
sprite.custom.px.offset_y = `-${ sprite.y }px`; // don't crop updos
|
||||
sprite.custom.px.offsetY = `-${ sprite.y }px`; // don't crop updos
|
||||
}
|
||||
}
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
let spritesheetSliceIndicies = calculateSpritesheetsSrcIndicies(src);
|
||||
let stream = mergeStream();
|
||||
|
||||
each(spritesheetSliceIndicies, (start, index) => {
|
||||
let slicedSrc = src.slice(start, spritesheetSliceIndicies[index + 1]);
|
||||
|
||||
let spriteData = gulp.src(slicedSrc)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}-${index}.png`,
|
||||
cssName: `spritesmith-${name}-${index}.css`,
|
||||
algorithm: 'binary-tree',
|
||||
padding: 1,
|
||||
cssTemplate: 'website/raw_sprites/css/css.template.handlebars',
|
||||
cssVarMap,
|
||||
}));
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH));
|
||||
|
||||
let cssStream = spriteData.css
|
||||
.pipe(gulp.dest(CSS_DIST_PATH));
|
||||
|
||||
stream.add(imgStream);
|
||||
stream.add(cssStream);
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:largeSprites', () => {
|
||||
let largeSrc = sync('website/raw_sprites/spritesmith_large/**/*.png');
|
||||
return createSpritesStream('largeSprites', largeSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', (done) => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
console.log('Verifiying that images do not exceed max dimensions'); // eslint-disable-line no-console
|
||||
|
||||
let numberOfSheetsThatAreTooBig = 0;
|
||||
|
||||
let distSpritesheets = sync(`${IMG_DIST_PATH}*.png`);
|
||||
|
||||
each(distSpritesheets, (img) => {
|
||||
let spriteSize = calculateImgDimensions(img);
|
||||
|
||||
if (spriteSize > MAX_SPRITESHEET_SIZE) {
|
||||
numberOfSheetsThatAreTooBig++;
|
||||
let name = basename(img, '.png');
|
||||
console.error(`WARNING: ${name} might be too big - ${spriteSize} > ${MAX_SPRITESHEET_SIZE}`); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfSheetsThatAreTooBig > 0) {
|
||||
// https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
|
||||
console.error( // eslint-disable-line no-console
|
||||
`${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.`);
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions'); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
pipe,
|
||||
awaitPort,
|
||||
kill,
|
||||
runMochaTests,
|
||||
} from './taskHelper';
|
||||
import { server as karma } from 'karma';
|
||||
import mongoose from 'mongoose';
|
||||
import { exec } from 'child_process';
|
||||
import psTree from 'ps-tree';
|
||||
import gulp from 'gulp';
|
||||
import Bluebird from 'bluebird';
|
||||
import runSequence from 'run-sequence';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import fs from 'fs';
|
||||
|
||||
const i18n = require('../website/server/libs/i18n');
|
||||
|
||||
// TODO rewrite
|
||||
|
||||
@@ -24,7 +15,6 @@ let server;
|
||||
|
||||
const TEST_DB_URI = nconf.get('TEST_DB_URI');
|
||||
|
||||
const API_V3_TEST_COMMAND = 'npm run test:api-v3';
|
||||
const SANITY_TEST_COMMAND = 'npm run test:sanity';
|
||||
const COMMON_TEST_COMMAND = 'npm run test:common';
|
||||
const CONTENT_TEST_COMMAND = 'npm run test:content';
|
||||
@@ -34,14 +24,14 @@ const CONTENT_OPTIONS = {maxBuffer: 1024 * 500};
|
||||
let testResults = [];
|
||||
let testCount = (stdout, regexp) => {
|
||||
let match = stdout.match(regexp);
|
||||
return parseInt(match && match[1] || 0);
|
||||
return parseInt(match && match[1] || 0, 10);
|
||||
};
|
||||
|
||||
let testBin = (string, additionalEnvVariables = '') => {
|
||||
if (os.platform() === 'win32') {
|
||||
if (additionalEnvVariables != '') {
|
||||
if (additionalEnvVariables !== '') {
|
||||
additionalEnvVariables = additionalEnvVariables.split(' ').join('&&set ');
|
||||
additionalEnvVariables = 'set ' + additionalEnvVariables + '&&';
|
||||
additionalEnvVariables = `set ${additionalEnvVariables}&&`;
|
||||
}
|
||||
return `set NODE_ENV=test&&${additionalEnvVariables}${string}`;
|
||||
} else {
|
||||
@@ -49,9 +39,9 @@ let testBin = (string, additionalEnvVariables = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task('test:nodemon', (done) => {
|
||||
process.env.PORT = TEST_SERVER_PORT;
|
||||
process.env.NODE_DB_URI = TEST_DB_URI;
|
||||
gulp.task('test:nodemon', () => {
|
||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
||||
|
||||
runSequence('nodemon');
|
||||
});
|
||||
@@ -68,8 +58,12 @@ gulp.task('test:prepare:mongo', (cb) => {
|
||||
gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
if (!server) {
|
||||
server = exec(testBin('node ./website/server/index.js', `NODE_DB_URI=${TEST_DB_URI} PORT=${TEST_SERVER_PORT}`), (error, stdout, stderr) => {
|
||||
if (error) { throw `Problem with the server: ${error}`; }
|
||||
if (stderr) { console.error(stderr); }
|
||||
if (error) {
|
||||
throw new Error(`Problem with the server: ${error}`);
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(stderr); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -84,7 +78,7 @@ gulp.task('test:prepare', [
|
||||
gulp.task('test:sanity', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SANITY_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -97,7 +91,7 @@ gulp.task('test:sanity', (cb) => {
|
||||
gulp.task('test:common', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -118,7 +112,7 @@ gulp.task('test:common:watch', ['test:common:clean'], () => {
|
||||
gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
testResults.push({
|
||||
suite: 'Common Specs\t',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
@@ -135,7 +129,7 @@ gulp.task('test:content', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -157,7 +151,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
(err, stdout, stderr) => {
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
testResults.push({
|
||||
suite: 'Content Specs\t',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
@@ -173,7 +167,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -192,7 +186,7 @@ gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -212,7 +206,7 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => done(err)
|
||||
(err) => done(err)
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gulp from 'gulp';
|
||||
import { postToSlack, conf } from './taskHelper';
|
||||
|
||||
@@ -12,8 +11,82 @@ const SLACK_CONFIG = {
|
||||
|
||||
const LOCALES = './website/common/locales/';
|
||||
const ENGLISH_LOCALE = `${LOCALES}en/`;
|
||||
|
||||
|
||||
function getArrayOfLanguages () {
|
||||
let languages = fs.readdirSync(LOCALES);
|
||||
languages.shift(); // Remove README.md from array of languages
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
const ALL_LANGUAGES = getArrayOfLanguages();
|
||||
|
||||
function stripOutNonJsonFiles (collection) {
|
||||
let onlyJson = _.filter(collection, (file) => {
|
||||
return file.match(/[a-zA-Z]*\.json/);
|
||||
});
|
||||
|
||||
return onlyJson;
|
||||
}
|
||||
|
||||
function eachTranslationFile (languages, cb) {
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
|
||||
_.each(languages, (lang) => {
|
||||
_.each(jsonFiles, (filename) => {
|
||||
let parsedTranslationFile;
|
||||
try {
|
||||
const translationFile = fs.readFileSync(`${LOCALES}${lang}/${filename}`);
|
||||
parsedTranslationFile = JSON.parse(translationFile);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
let englishFile = fs.readFileSync(ENGLISH_LOCALE + filename);
|
||||
let parsedEnglishFile = JSON.parse(englishFile);
|
||||
|
||||
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function eachTranslationString (languages, cb) {
|
||||
eachTranslationFile(languages, (error, language, filename, englishJSON, translationJSON) => {
|
||||
if (error) return;
|
||||
_.each(englishJSON, (string, key) => {
|
||||
const translationString = translationJSON[key];
|
||||
cb(language, filename, key, string, translationString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessageForPosting (msg, items) {
|
||||
let body = `*Warning:* ${msg}`;
|
||||
body += '\n\n```\n';
|
||||
body += items.join('\n');
|
||||
body += '\n```';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function getStringsWith (json, interpolationRegex) {
|
||||
let strings = {};
|
||||
|
||||
_.each(json, (fileName) => {
|
||||
const rawFile = fs.readFileSync(ENGLISH_LOCALE + fileName);
|
||||
const parsedJson = JSON.parse(rawFile);
|
||||
|
||||
strings[fileName] = {};
|
||||
_.each(parsedJson, (value, key) => {
|
||||
const match = value.match(interpolationRegex);
|
||||
if (match) strings[fileName][key] = match;
|
||||
});
|
||||
});
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
const malformedStringExceptions = {
|
||||
messageDropFood: true,
|
||||
armoireFood: true,
|
||||
@@ -23,7 +96,6 @@ const malformedStringExceptions = {
|
||||
gulp.task('transifex', ['transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings']);
|
||||
|
||||
gulp.task('transifex:missingFiles', () => {
|
||||
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationFile(ALL_LANGUAGES, (error) => {
|
||||
@@ -40,7 +112,6 @@ gulp.task('transifex:missingFiles', () => {
|
||||
});
|
||||
|
||||
gulp.task('transifex:missingStrings', () => {
|
||||
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationString(ALL_LANGUAGES, (language, filename, key, englishString, translationString) => {
|
||||
@@ -58,7 +129,6 @@ gulp.task('transifex:missingStrings', () => {
|
||||
});
|
||||
|
||||
gulp.task('transifex:malformedStrings', () => {
|
||||
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
let interpolationRegex = /<%= [a-zA-Z]* %>/g;
|
||||
let stringsToLookFor = getStringsWith(jsonFiles, interpolationRegex);
|
||||
@@ -66,25 +136,23 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
let stringsWithMalformedInterpolations = [];
|
||||
let stringsWithIncorrectNumberOfInterpolations = [];
|
||||
|
||||
let count = 0;
|
||||
_.each(ALL_LANGUAGES, function (lang) {
|
||||
|
||||
_.each(stringsToLookFor, function (strings, file) {
|
||||
let translationFile = fs.readFileSync(LOCALES + lang + '/' + file);
|
||||
_.each(ALL_LANGUAGES, (lang) => {
|
||||
_.each(stringsToLookFor, (strings, filename) => {
|
||||
let translationFile = fs.readFileSync(`${LOCALES}${lang}/${filename}`);
|
||||
let parsedTranslationFile = JSON.parse(translationFile);
|
||||
|
||||
_.each(strings, function (value, key) {
|
||||
_.each(strings, (value, key) => { // eslint-disable-line max-nested-callbacks
|
||||
let translationString = parsedTranslationFile[key];
|
||||
if (!translationString) return;
|
||||
|
||||
let englishOccurences = stringsToLookFor[file][key];
|
||||
let englishOccurences = stringsToLookFor[filename][key];
|
||||
let translationOccurences = translationString.match(interpolationRegex);
|
||||
|
||||
if (!translationOccurences) {
|
||||
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
let malformedString = `${lang} - ${filename} - ${key} - ${translationString}`;
|
||||
stringsWithMalformedInterpolations.push(malformedString);
|
||||
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
|
||||
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
let missingInterpolationString = `${lang} - ${filename} - ${key} - ${translationString}`;
|
||||
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
|
||||
}
|
||||
});
|
||||
@@ -103,74 +171,3 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
postToSlack(formattedMessage, SLACK_CONFIG);
|
||||
}
|
||||
});
|
||||
|
||||
function getArrayOfLanguages () {
|
||||
let languages = fs.readdirSync(LOCALES);
|
||||
languages.shift(); // Remove README.md from array of languages
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
function eachTranslationFile (languages, cb) {
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
|
||||
_.each(languages, (lang) => {
|
||||
_.each(jsonFiles, (filename) => {
|
||||
try {
|
||||
var translationFile = fs.readFileSync(LOCALES + lang + '/' + filename);
|
||||
var parsedTranslationFile = JSON.parse(translationFile);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
let englishFile = fs.readFileSync(ENGLISH_LOCALE + filename);
|
||||
let parsedEnglishFile = JSON.parse(englishFile);
|
||||
|
||||
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function eachTranslationString (languages, cb) {
|
||||
eachTranslationFile(languages, (error, language, filename, englishJSON, translationJSON) => {
|
||||
if (error) return;
|
||||
_.each(englishJSON, (string, key) => {
|
||||
var translationString = translationJSON[key];
|
||||
cb(language, filename, key, string, translationString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessageForPosting (msg, items) {
|
||||
let body = `*Warning:* ${msg}`;
|
||||
body += '\n\n```\n';
|
||||
body += items.join('\n');
|
||||
body += '\n```';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function getStringsWith (json, interpolationRegex) {
|
||||
var strings = {};
|
||||
|
||||
_.each(json, function (file_name) {
|
||||
var raw_file = fs.readFileSync(ENGLISH_LOCALE + file_name);
|
||||
var parsed_json = JSON.parse(raw_file);
|
||||
|
||||
strings[file_name] = {};
|
||||
_.each(parsed_json, function (value, key) {
|
||||
var match = value.match(interpolationRegex);
|
||||
if (match) strings[file_name][key] = match;
|
||||
});
|
||||
});
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
function stripOutNonJsonFiles (collection) {
|
||||
let onlyJson = _.filter(collection, (file) => {
|
||||
return file.match(/[a-zA-Z]*\.json/);
|
||||
});
|
||||
|
||||
return onlyJson;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resolve } from 'path';
|
||||
* Get access to configruable values
|
||||
*/
|
||||
nconf.argv().env().file({ file: 'config.json' });
|
||||
export var conf = nconf;
|
||||
export const conf = nconf;
|
||||
|
||||
/*
|
||||
* Kill a child process and any sub-children that process may have spawned.
|
||||
@@ -26,11 +26,12 @@ export function kill (proc) {
|
||||
pids.forEach(kill); return;
|
||||
}
|
||||
try {
|
||||
exec(/^win/.test(process.platform)
|
||||
? `taskkill /PID ${pid} /T /F`
|
||||
: `kill -9 ${pid}`);
|
||||
exec(/^win/.test(process.platform) ?
|
||||
`taskkill /PID ${pid} /T /F` :
|
||||
`kill -9 ${pid}`);
|
||||
} catch (e) {
|
||||
console.log(e); // eslint-disable-line no-console
|
||||
}
|
||||
catch (e) { console.log(e); }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,21 +45,25 @@ export function kill (proc) {
|
||||
* before failing.
|
||||
*/
|
||||
export function awaitPort (port, max = 60) {
|
||||
return new Bluebird((reject, resolve) => {
|
||||
let socket, timeout, interval;
|
||||
return new Bluebird((rej, res) => {
|
||||
let socket;
|
||||
let timeout;
|
||||
let interval;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject(`Timed out after ${max} seconds`);
|
||||
rej(`Timed out after ${max} seconds`);
|
||||
}, max * 1000);
|
||||
|
||||
interval = setInterval(() => {
|
||||
socket = net.connect({port: port}, () => {
|
||||
socket = net.connect({port}, () => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
socket.destroy();
|
||||
resolve();
|
||||
}).on('error', () => { socket.destroy; });
|
||||
res();
|
||||
}).on('error', () => {
|
||||
socket.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
@@ -67,8 +72,12 @@ export function awaitPort (port, max = 60) {
|
||||
* Pipe the child's stdin and stderr to the parent process.
|
||||
*/
|
||||
export function pipe (child) {
|
||||
child.stdout.on('data', (data) => { process.stdout.write(data); });
|
||||
child.stderr.on('data', (data) => { process.stderr.write(data); });
|
||||
child.stdout.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -78,8 +87,8 @@ export function postToSlack (msg, config = {}) {
|
||||
let slackUrl = nconf.get('SLACK_URL');
|
||||
|
||||
if (!slackUrl) {
|
||||
console.error('No slack post url specified. Your message was:');
|
||||
console.log(msg);
|
||||
console.error('No slack post url specified. Your message was:'); // eslint-disable-line no-console
|
||||
console.log(msg); // eslint-disable-line no-console
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -89,15 +98,15 @@ export function postToSlack (msg, config = {}) {
|
||||
channel: `#${config.channel || '#general'}`,
|
||||
username: config.username || 'gulp task',
|
||||
text: msg,
|
||||
icon_emoji: `:${config.emoji || 'gulp'}:`,
|
||||
icon_emoji: `:${config.emoji || 'gulp'}:`, // eslint-disable-line camelcase
|
||||
})
|
||||
.end((err, res) => {
|
||||
if (err) console.error('Unable to post to slack', err);
|
||||
.end((err) => {
|
||||
if (err) console.error('Unable to post to slack', err); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
export function runMochaTests (files, server, cb) {
|
||||
require('../test/helpers/globals.helper');
|
||||
require('../test/helpers/globals.helper'); // eslint-disable-line global-require
|
||||
|
||||
let mocha = new Mocha({reporter: 'spec'});
|
||||
let tests = glob(files);
|
||||
@@ -108,7 +117,7 @@ export function runMochaTests (files, server, cb) {
|
||||
});
|
||||
|
||||
mocha.run((numberOfFailures) => {
|
||||
if (!process.env.RUN_INTEGRATION_TEST_FOREVER) {
|
||||
if (!process.env.RUN_INTEGRATION_TEST_FOREVER) { // eslint-disable-line no-process-env
|
||||
if (server) kill(server);
|
||||
process.exit(numberOfFailures);
|
||||
}
|
||||
|
||||
11
gulpfile.js
11
gulpfile.js
@@ -8,11 +8,10 @@
|
||||
|
||||
require('babel-register');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
require('./gulp/gulp-apidoc');
|
||||
require('./gulp/gulp-build');
|
||||
require('./gulp/gulp-bootstrap');
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require);
|
||||
require('gulp').task('default', ['test']);
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
|
||||
require('gulp').task('default', ['test']); // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ var migrationName = '20140831_increase_gems_for_previous_contributions';
|
||||
* https://github.com/HabitRPG/habitrpg/issues/3933
|
||||
* Increase Number of Gems for Contributors
|
||||
* author: Alys (d904bd62-da08-416b-a816-ba797c9ee265)
|
||||
*
|
||||
*
|
||||
* Increase everyone's gems per their contribution level.
|
||||
* Originally they were given 2 gems per tier.
|
||||
* Now they are given 3 gems per tier for tiers 1,2,3
|
||||
@@ -70,7 +70,7 @@ dbUsers.findEach(query, fields, function(err, user) {
|
||||
var extraGems = tier; // tiers 1,2,3
|
||||
if (tier > 3) { extraGems = 3 + (tier - 3) * 2; }
|
||||
if (tier == 8) { extraGems = 11; }
|
||||
extraBalance = extraGems / 4;
|
||||
var extraBalance = extraGems / 4;
|
||||
set['balance'] = user.balance + extraBalance;
|
||||
|
||||
// Capture current state of user:
|
||||
|
||||
@@ -39,7 +39,7 @@ function findUsers(gt){
|
||||
console.log('User: ', countUsers, user._id);
|
||||
|
||||
var update = {
|
||||
$set: {};
|
||||
$set: {}
|
||||
};
|
||||
|
||||
if(user.auth && user.auth.local) {
|
||||
@@ -60,4 +60,4 @@ function findUsers(gt){
|
||||
});
|
||||
};
|
||||
|
||||
findUsers();
|
||||
findUsers();
|
||||
|
||||
111
migrations/20171030_jackolanterns.js
Normal file
111
migrations/20171030_jackolanterns.js
Normal file
@@ -0,0 +1,111 @@
|
||||
var migrationName = '20171030_jackolanterns.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 the Jack-O'-Lantern ladder:
|
||||
* Ghost Jack-O-Lantern Mount to owners of Ghost Jack-O-Lantern Pet
|
||||
* Ghost Jack-O-Lantern Pet to owners of Jack-O-Lantern Mount
|
||||
* Jack-O-Lantern Mount to owners of Jack-O-Lantern Pet
|
||||
* Jack-O-Lantern Pet 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.pets',
|
||||
'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 = {};
|
||||
var inc = {
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Ghost': true};
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost': 5};
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base': true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Base': 5};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set, $inc:inc});
|
||||
|
||||
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;
|
||||
128
migrations/20171117_turkey_ladder.js
Normal file
128
migrations/20171117_turkey_ladder.js
Normal file
@@ -0,0 +1,128 @@
|
||||
var migrationName = '20171117_turkey_ladder.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 the Turkey Day ladder:
|
||||
* Grant Turkey Costume to those who have the Gilded Turkey mount
|
||||
* Grant Gilded Turkey mount to those who have the Gilded Turkey pet
|
||||
* Grant Gilded Turkey pet to those who have the Base Turkey mount
|
||||
* Grant Base Turkey mount to those who have the Base Turkey pet
|
||||
* Grant Base Turkey pet to those who have none of the above yet
|
||||
*/
|
||||
|
||||
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},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2017-11-01')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.pets',
|
||||
'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 && user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
set = {
|
||||
migration: migrationName,
|
||||
'items.gear.owned.head_special_turkeyHelmBase': false,
|
||||
'items.gear.owned.armor_special_turkeyArmorBase': false,
|
||||
'items.gear.owned.back_special_turkeyTailBase': false,
|
||||
};
|
||||
var push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
];
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.Turkey-Base':5};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
if (push) {
|
||||
dbUsers.update({_id: user._id}, {$push: {pinnedItems: {$each: push}}});
|
||||
}
|
||||
|
||||
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;
|
||||
103
migrations/20171230_nye_hats.js
Normal file
103
migrations/20171230_nye_hats.js
Normal file
@@ -0,0 +1,103 @@
|
||||
var migrationName = '20171230_nye_hats.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 New Year's Eve party hats to users in sequence
|
||||
*/
|
||||
|
||||
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},
|
||||
'auth.timestamps.loggedin': {$gt:new Date('2017-11-30')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.gear.owned',
|
||||
] // 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 = {};
|
||||
var push = {};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2017':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2017', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2016', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2015', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2014', '_id': monk.id()}};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye', '_id': monk.id()}};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set, $push: push});
|
||||
|
||||
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;
|
||||
@@ -17,8 +17,5 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
var processUsers = require('./groups/update-groups-with-group-plans');
|
||||
processUsers()
|
||||
.catch(function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
const processUsers = require('./takeThis');
|
||||
processUsers();
|
||||
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['armor_mystery_201710','head_mystery_201710']
|
||||
$each:['armor_mystery_201712','head_mystery_201712']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var migrationName = '20170502_takeThis.js'; // Update per month
|
||||
var migrationName = '20180102_takeThis.js'; // Update per month
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
@@ -7,14 +7,14 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
|
||||
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},
|
||||
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
|
||||
'challenges':{$in:['5f70ce5b-2d82-4114-8e44-ca65615aae62']} // Update per month
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
@@ -65,19 +65,29 @@ function updateUser (user) {
|
||||
set = {'migration':migrationName};
|
||||
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', '_id': monk.id()}};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', '_id': monk.id()}};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
if (push) {
|
||||
dbUsers.update({_id: user._id}, {$set: set, $push: push});
|
||||
} else {
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
}
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
|
||||
88
migrations/tasks/tasks-set-everyX.js
Normal file
88
migrations/tasks/tasks-set-everyX.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var migrationName = 'tasks-set-everyX';
|
||||
var authorName = ''; // in case script author needs to know when their ...
|
||||
var authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* Iterates over all tasks and sets invalid everyX values (less than 0 or more than 9999 or not an int) field to 0
|
||||
*/
|
||||
|
||||
var monk = require('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 = {
|
||||
type: "daily",
|
||||
everyX: {
|
||||
$not: {
|
||||
$gte: 0,
|
||||
$lte: 9999,
|
||||
$type: "int",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbTasks.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [],
|
||||
})
|
||||
.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 = {'everyX': 0};
|
||||
|
||||
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;
|
||||
38
migrations/users/account-transfer.js
Normal file
38
migrations/users/account-transfer.js
Normal file
@@ -0,0 +1,38 @@
|
||||
var migrationName = 'AccountTransfer';
|
||||
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
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = '';
|
||||
const Users = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
import uniq from 'lodash/uniq';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
|
||||
module.exports = async function accountTransfer () {
|
||||
const fromAccountId = '';
|
||||
const toAccountId = '';
|
||||
|
||||
const fromAccount = await Users.findOne({_id: fromAccountId});
|
||||
const toAccount = await Users.findOne({_id: toAccountId});
|
||||
|
||||
const newMounts = Object.assign({}, fromAccount.items.mounts, toAccount.items.mounts);
|
||||
const newPets = Object.assign({}, fromAccount.items.pets, toAccount.items.pets);
|
||||
const newBackgrounds = Object.assign({}, fromAccount.purchased.background, toAccount.purchased.background);
|
||||
|
||||
await Users.update({_id: toAccountId}, {
|
||||
$set: {
|
||||
'items.pets': newPets,
|
||||
'items.mounts': newMounts,
|
||||
'purchased.background': newBackgrounds,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
};
|
||||
93
migrations/users/achievement-restore.js
Normal file
93
migrations/users/achievement-restore.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const migrationName = 'AchievementRestore';
|
||||
const authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
|
||||
const authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* This migraition will copy user data from prod to test
|
||||
*/
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost/new-habit';
|
||||
const Users = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const monkOld = require('monk');
|
||||
const oldConnectionSting = 'mongodb://localhost/old-habit';
|
||||
const UsersOld = monk(oldConnectionSting).get('users', { castIds: false });
|
||||
|
||||
function getAchievementUpdate (newUser, oldUser) {
|
||||
const oldAchievements = oldUser.achievements;
|
||||
const newAchievements = newUser.achievements;
|
||||
|
||||
let achievementsUpdate = Object.assign({}, newAchievements);
|
||||
|
||||
// ultimateGearSets
|
||||
if (!achievementsUpdate.ultimateGearSets && oldAchievements.ultimateGearSets) {
|
||||
achievementsUpdate.ultimateGearSets = oldAchievements.ultimateGearSets;
|
||||
} else if (oldAchievements.ultimateGearSets) {
|
||||
for (let index in oldAchievements.ultimateGearSets) {
|
||||
if (oldAchievements.ultimateGearSets[index]) achievementsUpdate.ultimateGearSets[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// challenges
|
||||
if (!newAchievements.challenges) newAchievements.challenges = [];
|
||||
if (!oldAchievements.challenges) oldAchievements.challenges = [];
|
||||
achievementsUpdate.challenges = newAchievements.challenges.concat(oldAchievements.challenges);
|
||||
|
||||
// Quests
|
||||
if (!achievementsUpdate.quests) achievementsUpdate.quests = {};
|
||||
for (let index in oldAchievements.quests) {
|
||||
if (!achievementsUpdate.quests[index]) {
|
||||
achievementsUpdate.quests[index] = oldAchievements.quests[index];
|
||||
} else {
|
||||
achievementsUpdate.quests[index] += oldAchievements.quests[index];
|
||||
}
|
||||
}
|
||||
|
||||
// Rebirth level
|
||||
if (achievementsUpdate.rebirthLevel) {
|
||||
achievementsUpdate.rebirthLevel = Math.max(achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel);
|
||||
} else if (oldAchievements.rebirthLevel) {
|
||||
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;
|
||||
}
|
||||
|
||||
//All others
|
||||
const indexsToIgnore = ['ultimateGearSets', 'challenges', 'quests', 'rebirthLevel'];
|
||||
for (let index in oldAchievements) {
|
||||
if (indexsToIgnore.indexOf(index) !== -1) continue;
|
||||
|
||||
if (!achievementsUpdate[index]) {
|
||||
achievementsUpdate[index] = oldAchievements[index];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Number.isInteger(oldAchievements[index])) {
|
||||
achievementsUpdate[index] += oldAchievements[index];
|
||||
} else {
|
||||
if (oldAchievements[index] === true) achievementsUpdate[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return achievementsUpdate;
|
||||
}
|
||||
|
||||
module.exports = async function achievementRestore () {
|
||||
const userIds = [
|
||||
];
|
||||
|
||||
for (let index in userIds) {
|
||||
const userId = userIds[index];
|
||||
const oldUser = await UsersOld.findOne({_id: userId}, 'achievements');
|
||||
const newUser = await Users.findOne({_id: userId}, 'achievements');
|
||||
const achievementUpdate = getAchievementUpdate(newUser, oldUser);
|
||||
await Users.update(
|
||||
{_id: userId},
|
||||
{
|
||||
$set: {
|
||||
'achievements': achievementUpdate,
|
||||
},
|
||||
});
|
||||
console.log(`Updated ${userId}`);
|
||||
}
|
||||
};
|
||||
2678
package-lock.json
generated
2678
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.6.2",
|
||||
"version": "4.18.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -31,8 +31,8 @@
|
||||
"bcrypt": "^1.0.2",
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "4.0.0-alpha.6",
|
||||
"bootstrap-vue": "1.0.0-beta.7",
|
||||
"bootstrap": "4.0.0-beta.2",
|
||||
"bootstrap-vue": "^1.0.2",
|
||||
"browserify": "~12.0.1",
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
@@ -116,13 +116,14 @@
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.1.0",
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue": "^2.5.2",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-router": "^3.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#45e607a7bccf4e3e089761b3b7b33e3f2c5dc21f",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"vuedraggable": "^2.15.0",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#825a866b6a9c52dd8c588a3e8b900880875ce914",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.1.0",
|
||||
@@ -136,7 +137,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
|
||||
"test": "npm run lint && gulp test && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
@@ -148,7 +149,7 @@
|
||||
"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": "gulp bootstrap && node webpack/dev-server.js",
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "gulp build:client",
|
||||
"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",
|
||||
|
||||
@@ -64,11 +64,11 @@ describe('GET /challenges/:challengeId/export/csv', () => {
|
||||
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
|
||||
let splitRes = res.split('\n');
|
||||
|
||||
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Task,Value,Notes');
|
||||
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
|
||||
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[5]).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('GET challenges/groups/:groupId', () => {
|
||||
context('Public Guild', () => {
|
||||
@@ -181,4 +182,123 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
expect(foundChallengeIndex).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
context('Party', () => {
|
||||
let party, user, nonMember, challenge, challenge2;
|
||||
|
||||
before(async () => {
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'TestParty',
|
||||
type: 'party',
|
||||
},
|
||||
});
|
||||
|
||||
party = group;
|
||||
user = groupLeader;
|
||||
|
||||
nonMember = await generateUser();
|
||||
|
||||
challenge = await generateChallenge(user, group);
|
||||
challenge2 = await generateChallenge(user, group);
|
||||
});
|
||||
|
||||
it('should prevent non-member from seeing challenges', async () => {
|
||||
await expect(nonMember.get(`/challenges/groups/${party._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return group challenges for member with populated leader', async () => {
|
||||
let challenges = await user.get(`/challenges/groups/${party._id}`);
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return group challenges for member using ID "party"', async () => {
|
||||
let challenges = await user.get('/challenges/groups/party');
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Tavern', () => {
|
||||
let tavern, user, challenge, challenge2;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({balance: 0.5});
|
||||
tavern = await user.get(`/groups/${TAVERN_ID}`);
|
||||
|
||||
challenge = await generateChallenge(user, tavern, {prize: 1});
|
||||
challenge2 = await generateChallenge(user, tavern, {prize: 1});
|
||||
});
|
||||
|
||||
it('should return tavern challenges with populated leader', async () => {
|
||||
let challenges = await user.get(`/challenges/groups/${TAVERN_ID}`);
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return tavern challenges using ID "habitrpg', async () => {
|
||||
let challenges = await user.get('/challenges/groups/habitrpg');
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
generateUser,
|
||||
translate as t,
|
||||
sleep,
|
||||
server,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils';
|
||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import nconf from 'nconf';
|
||||
@@ -96,6 +98,24 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat message contains a banned word in a public guild', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'public guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: bannedWordErrorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when word is part of a phrase', async () => {
|
||||
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
|
||||
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
|
||||
@@ -161,7 +181,7 @@ describe('POST /chat', () => {
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
|
||||
it('does not error when sending a chat message containing a banned word to a public guild in which banned words are allowed', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'public guild',
|
||||
@@ -171,6 +191,8 @@ describe('POST /chat', () => {
|
||||
members: 1,
|
||||
});
|
||||
|
||||
guildsAllowingBannedWords[group._id] = true;
|
||||
|
||||
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
@@ -342,6 +364,24 @@ describe('POST /chat', () => {
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('adds backer info to chat', async () => {
|
||||
const backerInfo = {
|
||||
npc: 'Town Crier',
|
||||
tier: 800,
|
||||
tokensApplied: true,
|
||||
};
|
||||
const backer = await generateUser({
|
||||
backer: backerInfo,
|
||||
});
|
||||
|
||||
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
const messageBackerInfo = message.message.backer;
|
||||
|
||||
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
|
||||
expect(messageBackerInfo.tier).to.equal(backerInfo.tier);
|
||||
expect(messageBackerInfo.tokensApplied).to.equal(backerInfo.tokensApplied);
|
||||
});
|
||||
|
||||
it('sends group chat received webhooks', async () => {
|
||||
let userUuid = generateUUID();
|
||||
let memberUuid = generateUUID();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import config from '../../../../../config.json';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
@@ -74,7 +75,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('can unflag a system message', async () => {
|
||||
it('can\'t flag a system message', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
type: 'party',
|
||||
@@ -95,13 +96,15 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
await member.post('/user/class/cast/mpheal');
|
||||
|
||||
let [skillMsg] = await member.get(`/groups/${group.id}/chat`);
|
||||
|
||||
await member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`);
|
||||
await admin.post(`/groups/${group._id}/chat/${skillMsg.id}/clearflags`);
|
||||
|
||||
let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
expect(messages[0].id).to.eql(skillMsg.id);
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
await expect(member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}),
|
||||
});
|
||||
// let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
// expect(messages[0].id).to.eql(skillMsg.id);
|
||||
// expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('GET /coupons/', () => {
|
||||
let user;
|
||||
@@ -19,7 +19,7 @@ describe('GET /coupons/', () => {
|
||||
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noSudoAccess'),
|
||||
message: apiMessages('noSudoAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import couponCode from 'coupon-code';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('POST /coupons/generate/:event', () => {
|
||||
let user;
|
||||
@@ -25,7 +26,7 @@ describe('POST /coupons/generate/:event', () => {
|
||||
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noSudoAccess'),
|
||||
message: apiMessages('noSudoAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => {
|
||||
let resIds = res.concat(res2).map(member => member._id);
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
|
||||
it('searches members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
usersToGenerate.push(generateUser({party: {_id: group._id}}));
|
||||
}
|
||||
const usersCreated = await Promise.all(usersToGenerate);
|
||||
const userToSearch = usersCreated[0].profile.name;
|
||||
|
||||
let res = await user.get(`/groups/party/members?search=${userToSearch}`);
|
||||
expect(res.length).to.equal(1);
|
||||
expect(res[0].profile.name).to.equal(userToSearch);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import * as payments from '../../../../../website/server/libs/payments';
|
||||
|
||||
describe('POST /groups/:groupId/leave', () => {
|
||||
let typesOfGroups = {
|
||||
@@ -264,4 +266,45 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
expect(userWithNonExistentParty.party).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
context('Leaving a group plan', () => {
|
||||
it('cancels the free subscription', async () => {
|
||||
// Create group
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Private Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let leader = groupLeader;
|
||||
let member = members[0];
|
||||
let userWithFreePlan = await User.findById(leader._id).exec();
|
||||
|
||||
// Create subscription
|
||||
let paymentData = {
|
||||
user: userWithFreePlan,
|
||||
groupId: group._id,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
await payments.createSubscription(paymentData);
|
||||
await member.sync();
|
||||
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
|
||||
expect(member.purchased.plan.dateTerminated).to.not.exist;
|
||||
|
||||
// Leave
|
||||
await member.post(`/groups/${group._id}/leave`);
|
||||
await member.sync();
|
||||
expect(member.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,4 +118,56 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
});
|
||||
const receiver = await generateUser({'inbox.blocks': [userToSendMessage._id]});
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('allows admin to send when to user has opted out of messaging', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
});
|
||||
const receiver = await generateUser({'inbox.optOut': true});
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,14 +13,44 @@ describe('POST /notifications/:notificationId/read', () => {
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(user.post(`/notifications/${dummyId}/read`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
|
||||
await expect(user.post(`/notifications/${dummyId}/read`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
xit('removes a notification', async () => {
|
||||
it('removes a notification', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
|
||||
const res = await user.post(`/notifications/${id}/read`);
|
||||
expect(res).to.deep.equal([{
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].id).to.equal(id2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/read', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.post('/notifications/read', {
|
||||
notificationIds: [dummyId],
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('removes multiple notifications', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
const id3 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}, {
|
||||
id: id3,
|
||||
type: 'CRON',
|
||||
data: {},
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
|
||||
const res = await user.post('/notifications/read', {
|
||||
notificationIds: [id, id3],
|
||||
});
|
||||
|
||||
expect(res).to.deep.equal([{
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].id).to.equal(id2);
|
||||
});
|
||||
});
|
||||
@@ -141,6 +141,16 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('removes a task from user.tasksOrder'); // TODO
|
||||
it('removes a task from user.tasksOrder', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.tasksOrder.habits.indexOf(task._id)).to.eql(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('uncompletes todo when direction is down', async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
await user.post(`/tasks/${todo._id}/score/down`);
|
||||
let updatedTask = await user.get(`/tasks/${todo._id}`);
|
||||
|
||||
@@ -137,9 +138,23 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(updatedTask.dateCompleted).to.be.a('undefined');
|
||||
});
|
||||
|
||||
it('scores up todo even if it is already completed'); // Yes?
|
||||
it('doesn\'t let a todo be completed twice', async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
await expect(user.post(`/tasks/${todo._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
it('scores down todo even if it is already uncompleted'); // Yes?
|
||||
it('doesn\'t let a todo be uncompleted twice', async () => {
|
||||
await expect(user.post(`/tasks/${todo._id}/score/down`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser;
|
||||
@@ -163,23 +178,25 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser;
|
||||
let updatedUser, initialUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
initialUser = await user.get('/user');
|
||||
await user.post(`/tasks/${todo._id}/score/down`);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -202,6 +219,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('uncompletes daily when direction is down', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
await user.post(`/tasks/${daily._id}/score/down`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
@@ -222,9 +240,22 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('scores up daily even if it is already completed'); // Yes?
|
||||
it('doesn\'t let a daily be completed twice', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
await expect(user.post(`/tasks/${daily._id}/score/up`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
it('scores down daily even if it is already uncompleted'); // Yes?
|
||||
it('doesn\'t let a daily be uncompleted twice', async () => {
|
||||
await expect(user.post(`/tasks/${daily._id}/score/down`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser;
|
||||
@@ -248,23 +279,25 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser;
|
||||
let updatedUser, initialUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
initialUser = await user.get('/user');
|
||||
await user.post(`/tasks/${daily._id}/score/down`);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,9 +40,13 @@ describe('POST /tasks/:taskId/move/to/:position', () => {
|
||||
|
||||
let taskToMove = tasks[1];
|
||||
expect(taskToMove.text).to.equal('habit 2');
|
||||
|
||||
let newOrder = await user.post(`/tasks/${tasks[1]._id}/move/to/3`);
|
||||
await user.sync();
|
||||
|
||||
expect(newOrder[3]).to.equal(taskToMove._id);
|
||||
expect(newOrder.length).to.equal(5);
|
||||
expect(user.tasksOrder.habits).to.eql(newOrder);
|
||||
});
|
||||
|
||||
it('can move task to new position using alias', async () => {
|
||||
|
||||
@@ -302,6 +302,17 @@ describe('POST /tasks/user', () => {
|
||||
|
||||
expect(task.alias).to.eql('a_alias012');
|
||||
});
|
||||
|
||||
// This is a special case for iOS requests
|
||||
it('will round a priority (difficulty)', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
priority: 0.10000000000005,
|
||||
});
|
||||
|
||||
expect(task.priority).to.eql(0.1);
|
||||
});
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
@@ -628,6 +639,43 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is a non int', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: 2.5,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is negative', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: -1,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is above 9999', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: 10000,
|
||||
})).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',
|
||||
|
||||
@@ -82,6 +82,13 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('should update the history', async () => {
|
||||
let newCron = new Date(2015, 11, 20);
|
||||
|
||||
await user.post('/debug/set-cron', {
|
||||
lastCron: newCron,
|
||||
});
|
||||
|
||||
await user.post('/cron');
|
||||
await user.post(`/tasks/${usersChallengeTaskId}/score/up`);
|
||||
|
||||
let tasks = await user.get(`/tasks/challenge/${challenge._id}`);
|
||||
|
||||
@@ -41,8 +41,9 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -58,6 +59,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
@@ -71,8 +73,9 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -88,6 +91,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
@@ -97,6 +101,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
@@ -75,15 +75,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unassigns a user from a task', async () => {
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
@@ -129,4 +120,26 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('allows a user to unassign themselves', async () => {
|
||||
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await user.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;
|
||||
});
|
||||
|
||||
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
|
||||
// here, but then we had a request to allow to unaissgn.
|
||||
xit('returns error when non leader tries to unassign their a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('DELETE /user', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('incorrectDeletePhrase'),
|
||||
message: t('incorrectDeletePhrase', {magicWord: 'DELETE'}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,4 +27,13 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns only user properties requested', async () => {
|
||||
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.achievements).to.exist;
|
||||
expect(returnedUser.items.mounts).to.exist;
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,12 +25,32 @@ describe('POST /user/buy-gear/:key', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('buys a piece of gear', async () => {
|
||||
it('buys the first level weapon gear', async () => {
|
||||
let key = 'weapon_warrior_0';
|
||||
|
||||
await user.post(`/user/buy-gear/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned[key]).to.eql(true);
|
||||
});
|
||||
|
||||
it('buys the first level armor gear', async () => {
|
||||
let key = 'armor_warrior_1';
|
||||
|
||||
await user.post(`/user/buy-gear/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
|
||||
expect(user.items.gear.owned[key]).to.eql(true);
|
||||
});
|
||||
|
||||
it('tries to buy subsequent, level gear', async () => {
|
||||
let key = 'armor_warrior_2';
|
||||
|
||||
return expect(user.post(`/user/buy-gear/${key}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You need to purchase a lower level gear before this one.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate', () => {
|
||||
let user;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-bulk', () => {
|
||||
let user;
|
||||
const statsUpdate = {
|
||||
stats: {
|
||||
con: 1,
|
||||
str: 2,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('returns an error if user does not have enough points', async () => {
|
||||
await expect(user.post('/user/allocate-bulk', statsUpdate))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notEnoughAttrPoints'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allocates attribute points', async () => {
|
||||
await user.update({'stats.points': 3});
|
||||
|
||||
await user.post('/user/allocate-bulk', statsUpdate);
|
||||
await user.sync();
|
||||
|
||||
expect(user.stats.con).to.equal(1);
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-now', () => {
|
||||
// More tests in common code unit tests
|
||||
@@ -1,739 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('checkout', () => {
|
||||
let user, orderReferenceId, headers;
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: amzLib.constants.SELLER_NOTE,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(confirmOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
expect(authorizeSpy).to.be.calledOnce;
|
||||
expect(authorizeSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
|
||||
expect(closeOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
user = new User();
|
||||
headers = {};
|
||||
orderReferenceId = 'orderReferenceId';
|
||||
|
||||
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
|
||||
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
|
||||
confirmOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
authorizeSpy = sinon.stub(amzLib, 'authorize');
|
||||
authorizeSpy.returnsPromise().resolves({});
|
||||
|
||||
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
|
||||
closeOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.returnsPromise().resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setOrderReferenceDetails.restore();
|
||||
amzLib.confirmOrderReference.restore();
|
||||
amzLib.authorize.restore();
|
||||
amzLib.closeOrderReference.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
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});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
|
||||
await amzLib.checkout({user, orderReferenceId, headers, gift});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazongAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
sub = {
|
||||
key: subKey,
|
||||
price: amount,
|
||||
};
|
||||
groupId = group._id;
|
||||
headers = {};
|
||||
|
||||
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
createSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a billingAgreementId', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes with amazon with price to existing users', async () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscription', () => {
|
||||
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
|
||||
let getBillingAgreementDetailsSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
headers = {};
|
||||
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
|
||||
getBillingAgreementDetailsSpy.returnsPromise().resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
|
||||
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(amzLib.cancelSubscription({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a user subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a group subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(amzLib.authorizeOnBillingAgreement);
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -365,6 +365,72 @@ describe('cron', () => {
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should remove completed todos from users taskOrder list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
let todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
user.tasksOrder.todos = tasksByType.todos.map(taskTodo => {
|
||||
return taskTodo._id;
|
||||
});
|
||||
// Since ideally tasksByType should not contain completed todos, fake ids should be filtered too
|
||||
user.tasksOrder.todos.push('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
expect(tasksByType.todos).to.be.lengthOf(2);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
expect(tasksByType.todos).to.be.lengthOf(2);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should preserve todos order in task list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
let todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
|
||||
// Set up user.tasksOrder list in a specific order
|
||||
user.tasksOrder.todos = tasksByType.todos.map(todoTask => {
|
||||
return todoTask._id;
|
||||
}).reverse();
|
||||
let original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
let listsAreEqual = true;
|
||||
user.tasksOrder.todos.forEach((taskId, index) => {
|
||||
if (original[index]._id !== taskId) {
|
||||
listsAreEqual = false;
|
||||
}
|
||||
});
|
||||
|
||||
expect(listsAreEqual);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(original.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dailys', () => {
|
||||
|
||||
@@ -191,7 +191,7 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledTwice;
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
|
||||
});
|
||||
|
||||
@@ -229,6 +229,77 @@ describe('payments/index', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Winter 2017-18 Gift-1-Get-1 Promotion', async () => {
|
||||
it('creates a gift subscription for purchaser and recipient if none exist', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
||||
user.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for recipient and creates a gift subscription for purchaser without sub', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscriptions for purchaser and recipient', async () => {
|
||||
user.purchased.plan = plan;
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('sends a private message about the promotion', async () => {
|
||||
await api.createSubscription(data);
|
||||
let msg = '\`Hello sender, you received 3 months of subscription as part of our holiday gift-giving promotion!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledTwice;
|
||||
expect(user.sendMessage).to.be.calledWith(user, { senderMsg: msg });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for self', () => {
|
||||
|
||||
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal file
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Cancel Subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
|
||||
let getBillingAgreementDetailsSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelUserSubscriptionSpy () {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectAmazonCancelGroupSubscriptionSpy (groupId) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectBillingAggreementDetailSpy () {
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
headers = {};
|
||||
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
|
||||
getBillingAgreementDetailsSpy.returnsPromise().resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
|
||||
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(amzLib.cancelSubscription({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a user subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a group subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, orderReferenceId, headers;
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
|
||||
function expectOrderReferenceSpy () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: amzLib.constants.SELLER_NOTE,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAuthorizeSpy () {
|
||||
expect(authorizeSpy).to.be.calledOnce;
|
||||
expect(authorizeSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expectOrderReferenceSpy();
|
||||
|
||||
expect(confirmOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
expectAuthorizeSpy();
|
||||
|
||||
expect(closeOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
user = new User();
|
||||
headers = {};
|
||||
orderReferenceId = 'orderReferenceId';
|
||||
|
||||
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
|
||||
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
|
||||
confirmOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
authorizeSpy = sinon.stub(amzLib, 'authorize');
|
||||
authorizeSpy.returnsPromise().resolves({});
|
||||
|
||||
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
|
||||
closeOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.returnsPromise().resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setOrderReferenceDetails.restore();
|
||||
amzLib.confirmOrderReference.restore();
|
||||
amzLib.authorize.restore();
|
||||
amzLib.closeOrderReference.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectBuyGemsStub (paymentMethod, gift) {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
|
||||
let expectedArgs = {
|
||||
user,
|
||||
paymentMethod,
|
||||
headers,
|
||||
};
|
||||
if (gift) expectedArgs.gift = gift;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
|
||||
}
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
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});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
|
||||
await amzLib.checkout({user, orderReferenceId, headers, gift});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
});
|
||||
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal file
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazonAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
sub = {
|
||||
key: subKey,
|
||||
price: amount,
|
||||
};
|
||||
groupId = group._id;
|
||||
headers = {};
|
||||
|
||||
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
createSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectAmazonAuthorizeBillingAgreementSpy () {
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonSetBillingAgreementDetailsSpy () {
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectCreateSpy () {
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
}
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a billingAgreementId', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectCreateSpy();
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
|
||||
expectCreateSpy();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with price to existing users', async () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
expectCreateSpy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(amzLib.authorizeOnBillingAgreement);
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -475,7 +475,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Paypal)', async () => {
|
||||
|
||||
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal file
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
|
||||
export async function createNonLeaderGroupMember (group) {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
return await nonLeader.save();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
|
||||
describe('checkout success', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, gift, customerId, paymentId;
|
||||
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
customerId = 'customerId-test';
|
||||
paymentId = 'paymentId-test';
|
||||
|
||||
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentExecute.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal file
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
function getPaypalCreateOptions (description, amount) {
|
||||
return {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quantity: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approval_href';
|
||||
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentCreate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
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('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
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;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('creates a link for gifting a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
});
|
||||
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal file
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
|
||||
describe('ipn', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, txn_type, userPaymentId, groupPaymentId;
|
||||
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
txn_type = 'recurring_payment_profile_cancel';
|
||||
userPaymentId = 'userPaymentId-test';
|
||||
groupPaymentId = 'groupPaymentId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = userPaymentId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupPaymentId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.ipnVerifyAsync.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
});
|
||||
});
|
||||
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
|
||||
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
customerId = 'customer-id';
|
||||
groupCustomerId = 'groupCustomerId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: {
|
||||
next_billing_date: nextBillingDate,
|
||||
cycles_completed: 1,
|
||||
},
|
||||
});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user, groupId: group._id});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, block, groupId, token, headers, customerId;
|
||||
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
block = common.content.subscriptionBlocks[subKey];
|
||||
customerId = 'test-customerId';
|
||||
|
||||
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
|
||||
.returnsPromise({}).resolves({
|
||||
id: customerId,
|
||||
});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementExecute.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('create a group subscription', async () => {
|
||||
groupId = group._id;
|
||||
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal file
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable camelcase */
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let coupon, sub, approvalHerf;
|
||||
let paypalBillingAgreementCreateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approvalHerf-test';
|
||||
sub = Object.assign({}, common.content.subscriptionBlocks[subKey]);
|
||||
|
||||
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementCreate.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for a subscription', async () => {
|
||||
delete sub.discount;
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('cancel subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
|
||||
currentPeriodEndTimeStamp = (new Date()).getTime();
|
||||
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
|
||||
.returnsPromise().resolves({
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import stripeModule from 'stripe';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout with subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token;
|
||||
let spy;
|
||||
let stripeCreateCustomerSpy;
|
||||
let stripePaymentsCreateSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
sub = {
|
||||
key: 'basic_3mo',
|
||||
};
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub,
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
};
|
||||
|
||||
email = 'example@example.com';
|
||||
customerIdResponse = 'test-id';
|
||||
subscriptionId = 'test-sub-id';
|
||||
token = 'test-token';
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves;
|
||||
|
||||
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId}],
|
||||
},
|
||||
};
|
||||
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
|
||||
|
||||
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
stripe.customers.create.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a token', async () => {
|
||||
await expect(stripePayments.checkout({
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes a user', async () => {
|
||||
sub = data.sub;
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
headers = {};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 3,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group with the correct number of group members', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
headers = {};
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 4,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
|
||||
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
token = 'test-token';
|
||||
|
||||
customerIdResponse = 'example-customerIdResponse';
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
};
|
||||
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.charges.create.restore();
|
||||
payments.buyGems.restore();
|
||||
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,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: 500,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '400',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '1500',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('edit subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, group, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
|
||||
token = 'test-token';
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if a token is not provided', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
|
||||
.returnsPromise().resolves({
|
||||
data: [{id: subscriptionId}],
|
||||
});
|
||||
|
||||
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.listSubscriptions.restore();
|
||||
stripe.customers.updateSubscription.restore();
|
||||
});
|
||||
|
||||
it('edits a user subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
user.purchased.plan.customerId,
|
||||
subscriptionId,
|
||||
{ card: token }
|
||||
);
|
||||
});
|
||||
|
||||
it('edits a group subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
group.purchased.plan.customerId,
|
||||
subscriptionId,
|
||||
{ card: token }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
257
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
257
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
import logger from '../../../../../../../website/server/libs/logger';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const stripe = stripeModule('test');
|
||||
|
||||
describe('all events', () => {
|
||||
const eventType = 'account.updated';
|
||||
const event = {id: 123};
|
||||
const eventRetrieved = {type: eventType};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
|
||||
sinon.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.events.retrieve.restore();
|
||||
logger.error.restore();
|
||||
});
|
||||
|
||||
it('logs an error if an unsupported webhook event is passed', async () => {
|
||||
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(logger.error).to.have.been.called.once;
|
||||
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
|
||||
});
|
||||
|
||||
it('retrieves and validates the event from Stripe', async () => {
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customer.subscription.deleted', () => {
|
||||
const eventType = 'customer.subscription.deleted';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
request: 123,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
describe('user subscription', () => {
|
||||
it('throws an error if the user is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
const customerId = '456';
|
||||
|
||||
let subscriber = new User();
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('group plan subscription', () => {
|
||||
it('throws an error if the group is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('groupNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('throws an error if the group leader is not found', async () => {
|
||||
const customerId = 456;
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: uuid(),
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
const customerId = '456';
|
||||
|
||||
let leader = new User();
|
||||
await leader.save();
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: leader._id,
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
|
||||
describe('Stripe - Upgrade Group Plan', () => {
|
||||
const stripe = stripeModule('test');
|
||||
let spy, data, user, group;
|
||||
|
||||
beforeEach(async function () {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
stripePayments.setStripeApi(stripe);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
});
|
||||
@@ -1,561 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Paypal Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('checkout', () => {
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
function getPaypalCreateOptions (description, amount) {
|
||||
return {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quantity: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approval_href';
|
||||
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentCreate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
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('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
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;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('creates a link for gifting a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkout success', () => {
|
||||
let user, gift, customerId, paymentId;
|
||||
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
customerId = 'customerId-test';
|
||||
paymentId = 'paymentId-test';
|
||||
|
||||
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentExecute.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let coupon, sub, approvalHerf;
|
||||
let paypalBillingAgreementCreateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approvalHerf-test';
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementCreate.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for a subscription', async () => {
|
||||
delete sub.discount;
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
let user, group, block, groupId, token, headers, customerId;
|
||||
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
block = common.content.subscriptionBlocks[subKey];
|
||||
customerId = 'test-customerId';
|
||||
|
||||
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
|
||||
.returnsPromise({}).resolves({
|
||||
id: customerId,
|
||||
});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementExecute.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('create a group subscription', async () => {
|
||||
groupId = group._id;
|
||||
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
|
||||
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
customerId = 'customer-id';
|
||||
groupCustomerId = 'groupCustomerId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: {
|
||||
next_billing_date: nextBillingDate,
|
||||
cycles_completed: 1,
|
||||
},
|
||||
});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user, groupId: group._id});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipn', () => {
|
||||
let user, group, txn_type, userPaymentId, groupPaymentId;
|
||||
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
txn_type = 'recurring_payment_profile_cancel';
|
||||
userPaymentId = 'userPaymentId-test';
|
||||
groupPaymentId = 'groupPaymentId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = userPaymentId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupPaymentId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.ipnVerifyAsync.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,9 @@ import analyticsService from '../../../../../website/server/libs/analyticsServic
|
||||
import * as cronLib from '../../../../../website/server/libs/cron';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res, req;
|
||||
let user;
|
||||
@@ -235,7 +238,13 @@ describe('cron middleware', () => {
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'update')
|
||||
.withArgs({ _id: user._id, _cronSignature: 'NOT_RUNNING' })
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{_cronSignature: 'NOT_RUNNING'},
|
||||
{_cronSignature: {$lt: sinon.match.number}},
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
@@ -251,4 +260,48 @@ describe('cron middleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let now = new Date();
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
let expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, (err) => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let now = new Date();
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, (err) => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { NotAuthorized } from '../../../../../website/server/libs/errors';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('ensure access middlewares', () => {
|
||||
let res, req, next;
|
||||
@@ -42,7 +43,7 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
ensureSudo(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noSudoAccess')));
|
||||
expect(next).to.be.calledWith(new NotAuthorized(apiMessages('noSudoAccess')));
|
||||
});
|
||||
|
||||
it('passes when user is a sudo user', () => {
|
||||
|
||||
@@ -391,6 +391,20 @@ describe('Group Model', () => {
|
||||
expect(party.quest.progress.collect.soapBars).to.eq(5);
|
||||
});
|
||||
|
||||
it('does not drop an item if not need when on a collection quest', async () => {
|
||||
party.quest.key = 'dilatoryDistress1';
|
||||
party.quest.active = false;
|
||||
await party.startQuest(questLeader);
|
||||
party.quest.progress.collect.fireCoral = 20;
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(party.quest.progress.collect.fireCoral).to.eq(20);
|
||||
});
|
||||
|
||||
it('sends a chat message about progress', async () => {
|
||||
await Group.processQuestProgress(participatingMember, progress);
|
||||
|
||||
|
||||
@@ -323,4 +323,48 @@ describe('User Model', () => {
|
||||
expect(user.achievements.beastMaster).to.not.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('days missed', () => {
|
||||
// http://forbrains.co.uk/international_tools/earth_timezones
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone', () => {
|
||||
const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const timezoneOffset = moment().zone('-06:00').zone();
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
|
||||
const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas
|
||||
const req = {};
|
||||
req.header = () => {
|
||||
return timezoneOffset + 60;
|
||||
};
|
||||
|
||||
const {daysMissed} = user.daysUserHasMissed(today, req);
|
||||
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
|
||||
it('should not cron early when going back a timezone with a custom day start', () => {
|
||||
const yesterday = moment('2017-12-05T02:00:00.000-08:00');
|
||||
const timezoneOffset = moment().zone('-08:00').zone();
|
||||
user.lastCron = yesterday;
|
||||
user.preferences.timezoneOffset = timezoneOffset;
|
||||
user.preferences.dayStart = 2;
|
||||
|
||||
const today = moment('2017-12-06T02:00:00.000-08:00');
|
||||
const req = {};
|
||||
req.header = () => {
|
||||
return timezoneOffset + 60;
|
||||
};
|
||||
|
||||
const {daysMissed} = user.daysUserHasMissed(today, req);
|
||||
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
31
test/client/unit/specs/components/groups/membersModal.js
Normal file
31
test/client/unit/specs/components/groups/membersModal.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Vue from 'vue';
|
||||
import MembersModalComponent from 'client/components/groups/membersModal.vue';
|
||||
|
||||
describe('Members Modal Component', () => {
|
||||
describe('Party Sort', () => {
|
||||
let CTor;
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
CTor = Vue.extend(MembersModalComponent);
|
||||
vm = new CTor().$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('should have an empty object as sort-option at start', () => {
|
||||
const defaultData = vm.data();
|
||||
expect(defaultData.sortOption).to.eq({});
|
||||
});
|
||||
|
||||
it('should accept sort-option object', () => {
|
||||
const sortOption = vm.data().sortOption[0];
|
||||
vm.sort(sortOption);
|
||||
Vue.nextTick(() => {
|
||||
expect(vm.data().sortOption).to.eq(sortOption);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,7 +38,7 @@ describe('shared.ops.addTask', () => {
|
||||
expect(habit.counterDown).to.equal(0);
|
||||
});
|
||||
|
||||
it('adds an habtit when type is invalid', () => {
|
||||
it('adds a habit when type is invalid', () => {
|
||||
let habit = addTask(user, {
|
||||
body: {
|
||||
type: 'invalid',
|
||||
@@ -127,14 +127,6 @@ describe('shared.ops.addTask', () => {
|
||||
expect(addTask(user)._edit).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('respects tagsCollapsed preference', () => {
|
||||
user.preferences.tagsCollapsed = true;
|
||||
expect(addTask(user)._tags).to.not.be.ok;
|
||||
|
||||
user.preferences.tagsCollapsed = false;
|
||||
expect(addTask(user)._tags).to.be.ok;
|
||||
});
|
||||
|
||||
it('respects advancedCollapsed preference', () => {
|
||||
user.preferences.advancedCollapsed = true;
|
||||
expect(addTask(user)._advanced).not.be.ok;
|
||||
|
||||
@@ -231,13 +231,30 @@ describe('shared.ops.purchase', () => {
|
||||
context('bulk purchase', () => {
|
||||
let userGemAmount = 10;
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
user.balance = userGemAmount;
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = 0;
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
});
|
||||
|
||||
it('errors when user does not have enough gems', (done) => {
|
||||
user.balance = 1;
|
||||
let type = 'eggs';
|
||||
let key = 'TigerCub';
|
||||
|
||||
try {
|
||||
purchase(user, {
|
||||
params: {type, key},
|
||||
quantity: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughGems'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('makes bulk purchases of gems', () => {
|
||||
let [, message] = purchase(user, {
|
||||
params: {type: 'gems', key: 'gem'},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import allocate from '../../../website/common/script/ops/allocate';
|
||||
import allocate from '../../../../website/common/script/ops/stats/allocate';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocate', () => {
|
||||
let user;
|
||||
98
test/common/ops/stats/allocateBulk.js
Normal file
98
test/common/ops/stats/allocateBulk.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import allocateBulk from '../../../../website/common/script/ops/stats/allocateBulk';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateBulk', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('throws an error if an invalid attribute is supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
invalid: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'invalid'}));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the stats are not supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('statsObjectRequired'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have attribute points', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have enough attribute points', (done) => {
|
||||
user.stats.points = 1;
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('allocates attribute points', () => {
|
||||
user.stats.points = 3;
|
||||
expect(user.stats.int).to.equal(0);
|
||||
expect(user.stats.str).to.equal(0);
|
||||
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.int).to.equal(1);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import allocateNow from '../../../website/common/script/ops/allocateNow';
|
||||
import allocateNow from '../../../../website/common/script/ops/stats/allocateNow';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateNow', () => {
|
||||
let user;
|
||||
@@ -644,7 +644,27 @@ describe('shouldDo', () => {
|
||||
day = moment();
|
||||
dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
|
||||
dailyTask.everyX = 3;
|
||||
let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
|
||||
const threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
|
||||
|
||||
expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
it('activates Daily on every (x) week on weekday across a year', () => {
|
||||
dailyTask.repeat = {
|
||||
su: false,
|
||||
s: false,
|
||||
f: false,
|
||||
th: false,
|
||||
w: false,
|
||||
t: false,
|
||||
m: false,
|
||||
};
|
||||
|
||||
day = moment('2017-11-19');
|
||||
dailyTask.startDate = day.toDate();
|
||||
dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
|
||||
dailyTask.everyX = 3;
|
||||
const threeWeeksFromToday = moment('2018-01-21');
|
||||
|
||||
expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
@@ -970,7 +990,7 @@ describe('shouldDo', () => {
|
||||
m: false,
|
||||
};
|
||||
|
||||
let today = moment('2017-01-26');
|
||||
let today = moment('2017-01-26:00:00.000-00:00');
|
||||
let week = today.monthWeek();
|
||||
let dayOfWeek = today.day();
|
||||
dailyTask.startDate = today.toDate();
|
||||
@@ -979,7 +999,7 @@ describe('shouldDo', () => {
|
||||
dailyTask.everyX = 2;
|
||||
dailyTask.frequency = 'monthly';
|
||||
|
||||
day = moment('2017-03-24');
|
||||
day = moment('2017-03-24:00:00.000-00:00');
|
||||
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||
});
|
||||
@@ -995,7 +1015,7 @@ describe('shouldDo', () => {
|
||||
m: false,
|
||||
};
|
||||
|
||||
let today = moment('2017-01-27');
|
||||
let today = moment('2017-01-27:00:00.000-00:00');
|
||||
let week = today.monthWeek();
|
||||
let dayOfWeek = today.day();
|
||||
dailyTask.startDate = today.toDate();
|
||||
@@ -1004,7 +1024,7 @@ describe('shouldDo', () => {
|
||||
dailyTask.everyX = 2;
|
||||
dailyTask.frequency = 'monthly';
|
||||
|
||||
day = moment('2017-03-24');
|
||||
day = moment('2017-03-24:00:00.000-00:00');
|
||||
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
@@ -1020,7 +1040,7 @@ describe('shouldDo', () => {
|
||||
m: false,
|
||||
};
|
||||
|
||||
let today = moment('2017-01-27');
|
||||
let today = moment('2017-01-27:00:00.000-00:00');
|
||||
let week = today.monthWeek();
|
||||
let dayOfWeek = today.day();
|
||||
dailyTask.startDate = today.toDate();
|
||||
@@ -1029,7 +1049,7 @@ describe('shouldDo', () => {
|
||||
dailyTask.everyX = 2;
|
||||
dailyTask.frequency = 'monthly';
|
||||
|
||||
day = moment('2017-03-24');
|
||||
day = moment('2017-03-24:00:00.000-00:00');
|
||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||
});
|
||||
|
||||
|
||||
0
vagrant_scripts/install_node.sh
Normal file → Executable file
0
vagrant_scripts/install_node.sh
Normal file → Executable file
@@ -3,6 +3,14 @@ const path = require('path');
|
||||
const staticAssetsDirectory = './website/static/.'; // The folder where static files (not processed) live
|
||||
const prodEnv = require('./prod.env');
|
||||
const devEnv = require('./dev.env');
|
||||
const nconf = require('nconf');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
||||
|
||||
let configFile = path.join(path.resolve(__dirname, '../../config.json'));
|
||||
|
||||
setupNconf(configFile);
|
||||
|
||||
const DEV_BASE_URL = nconf.get('BASE_URL');
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
@@ -33,25 +41,25 @@ module.exports = {
|
||||
assetsPublicPath: '/',
|
||||
staticAssetsDirectory,
|
||||
proxyTable: {
|
||||
// proxy all requests starting with /api/v3 to localhost:3000
|
||||
// proxy all requests starting with /api/v3 to IP:PORT as specified in the top-level config
|
||||
'/api/v3': {
|
||||
target: 'http://localhost:3000',
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/stripe': {
|
||||
target: 'http://localhost:3000',
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/amazon': {
|
||||
target: 'http://localhost:3000',
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/paypal': {
|
||||
target: 'http://localhost:3000',
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/logout': {
|
||||
target: 'http://localhost:3000',
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
const nconf = require('nconf');
|
||||
const { join, resolve } = require('path');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
||||
|
||||
const PATH_TO_CONFIG = join(resolve(__dirname, '../../config.json'));
|
||||
let configFile = PATH_TO_CONFIG;
|
||||
|
||||
nconf
|
||||
.argv()
|
||||
.env()
|
||||
.file('user', configFile);
|
||||
|
||||
nconf.set('IS_PROD', nconf.get('NODE_ENV') === 'production');
|
||||
nconf.set('IS_DEV', nconf.get('NODE_ENV') === 'development');
|
||||
nconf.set('IS_TEST', nconf.get('NODE_ENV') === 'test');
|
||||
setupNconf(configFile);
|
||||
|
||||
// @TODO: Check if we can import from client. Items like admin emails can be imported
|
||||
// and that should be prefered
|
||||
@@ -40,7 +34,7 @@ let env = {
|
||||
},
|
||||
};
|
||||
|
||||
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED'
|
||||
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED LOGGLY_CLIENT_TOKEN'
|
||||
.split(' ')
|
||||
.forEach(key => {
|
||||
env[key] = `"${nconf.get(key)}"`;
|
||||
|
||||
@@ -11,7 +11,7 @@ const IS_PROD = process.env.NODE_ENV === 'production';
|
||||
|
||||
const baseConfig = {
|
||||
entry: {
|
||||
app: './website/client/main.js',
|
||||
app: ['babel-polyfill', './website/client/main.js'],
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
@@ -83,7 +83,7 @@ const baseConfig = {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
|
||||
name: utils.assetsPath('images/[name].[hash:7].[ext]'),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -109,6 +109,7 @@ const baseConfig = {
|
||||
loader: 'svg-url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('svg/[name].[hash:7].[ext]'),
|
||||
},
|
||||
},
|
||||
{ loader: 'svgo-loader' },
|
||||
|
||||
@@ -1,37 +1,68 @@
|
||||
<template lang="pug">
|
||||
#app(:class='{"casting-spell": castingSpell}')
|
||||
snackbars
|
||||
router-view(v-if="!isUserLoggedIn || isStaticPage")
|
||||
template(v-else)
|
||||
template(v-if="isUserLoaded")
|
||||
notifications-display
|
||||
app-menu
|
||||
.container-fluid
|
||||
app-header
|
||||
buyModal(
|
||||
:item="selectedItemToBuy || {}",
|
||||
:withPin="true",
|
||||
@change="resetItemToBuy($event)",
|
||||
@buyPressed="customPurchase($event)",
|
||||
:genericPurchase="genericPurchase(selectedItemToBuy)",
|
||||
div
|
||||
#loading-screen-inapp(v-if='loading')
|
||||
.row
|
||||
.col-12.text-center
|
||||
svg#melior(xmlns='http://www.w3.org/2000/svg', viewbox='0 0 61.91 64')
|
||||
path(d='M61.82,64H51.59c-3.08,0-3.72.37-3.67-1,0.07-1.87.67-1.94,2.63-2.49,1.63-.45,1-3.35-0.8-5.88-1.28-1.76-3.89-3.81-7.31-2.22a10.75,10.75,0,0,0-4.56,3.52c-1.68,2.33-1.59,4.54,1,4.54s5.39-1.5,6.23.64c1,2.64.33,2.89-.18,2.89H28.55v0C19.77,64,11,63.93,9,58.38c-2.82-7.68,7.43-10.64,7.75-15.46,0.13-2-1-2.85-2.34-2.85h-6V36.41H4.7v-11H8.36V29.1H12v3.65h3.65v5.08a5.76,5.76,0,0,1,3.07,5.05c-0.17,5.51-9.5,8.57-7.79,14.35,1.56,5.29,13.37,4,13,.74L23.7,56.1c-0.06-2.62-.47-6.12.08-9.22C24.64,42,27.67,37.78,33,37.74c1,0,1.78-.21,1.78-1s-1.55-.84-2.64-0.95a23.35,23.35,0,0,1-12.56-5c-2.43-2-6.21-8.3-3.74-7.83a21.74,21.74,0,0,0,4.06.4c1.24,0,4.44-.35,4.44-1.11,0-1-1.85-.42-4.57-0.68C16.48,21.22,9.6,19.83,6,9.35,4.71,5.43,3.83-1.91,6,.46c12.46,13.7,16.69,11.47,23.84,16.16,3.15,2.06,5.19,7,7,6.58,1.2-.27.46-1.37,0.64-3.93C37.66,17,38.75,16.48,36,15.79c-3.26-.81-6.52-4.38-4.39-4.33a11.89,11.89,0,0,0,5.53-.76c1.87-.81,6.43-4.28,9.18-2.89s5.08-.6,6.94-0.25c2.71,0.51,3.41,4.24,3.05,6.42-0.22,1.38-.22,1.38-2,1.28-3.61-.21-4.53,2.67-2,4.25,3.87,2.42,5.51,4.23,6.56,9.58,0.51,2.6.1,3.2-.76,2.72s-2.34-.72-0.29,4-1.29,10.28-2.39,10.9a1.3,1.3,0,0,0-.91,1.34c0,11.42,0,12.27,1.92,12.48,2.9,0.31,4.14-1.44,5.27.06C63.29,62.73,63.41,64,61.82,64ZM4.7,21.28H1v3.65H4.7V21.28Z', transform='translate(-1.05)', fill='#fff')
|
||||
.col-12.text-center
|
||||
h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}}
|
||||
p {{currentTip}}
|
||||
#app(:class='{"casting-spell": castingSpell}')
|
||||
amazon-payments-modal
|
||||
snackbars
|
||||
router-view(v-if="!isUserLoggedIn || isStaticPage")
|
||||
template(v-else)
|
||||
template(v-if="isUserLoaded")
|
||||
notifications-display
|
||||
app-menu
|
||||
.container-fluid
|
||||
app-header
|
||||
buyModal(
|
||||
:item="selectedItemToBuy || {}",
|
||||
:withPin="true",
|
||||
@change="resetItemToBuy($event)",
|
||||
@buyPressed="customPurchase($event)",
|
||||
:genericPurchase="genericPurchase(selectedItemToBuy)",
|
||||
|
||||
)
|
||||
selectMembersModal(
|
||||
:item="selectedSpellToBuy || {}",
|
||||
:group="user.party",
|
||||
@memberSelected="memberSelected($event)",
|
||||
)
|
||||
)
|
||||
selectMembersModal(
|
||||
:item="selectedSpellToBuy || {}",
|
||||
:group="user.party",
|
||||
@memberSelected="memberSelected($event)",
|
||||
)
|
||||
|
||||
div(:class='{sticky: user.preferences.stickyHeader}')
|
||||
router-view
|
||||
app-footer
|
||||
div(:class='{sticky: user.preferences.stickyHeader}')
|
||||
router-view
|
||||
app-footer
|
||||
|
||||
audio#sound(autoplay, ref="sound")
|
||||
source#oggSource(type="audio/ogg", :src="sound.oggSource")
|
||||
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
|
||||
audio#sound(autoplay, ref="sound")
|
||||
source#oggSource(type="audio/ogg", :src="sound.oggSource")
|
||||
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style lang='scss' scoped>
|
||||
#loading-screen-inapp {
|
||||
#melior {
|
||||
margin: 0 auto;
|
||||
width: 70.9px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
width: 448px;
|
||||
font-size: 24px;
|
||||
color: #d5c8ff;
|
||||
}
|
||||
}
|
||||
|
||||
.casting-spell {
|
||||
cursor: crosshair;
|
||||
}
|
||||
@@ -71,8 +102,8 @@
|
||||
import axios from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import AppMenu from './components/appMenu';
|
||||
import AppHeader from './components/appHeader';
|
||||
import AppMenu from './components/header/menu';
|
||||
import AppHeader from './components/header/index';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
@@ -82,6 +113,7 @@ import BuyModal from './components/shops/buyModal.vue';
|
||||
import SelectMembersModal from 'client/components/selectMembersModal.vue';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import { setup as setupPayments } from 'client/libs/payments';
|
||||
import amazonPaymentsModal from 'client/components/payments/amazonModal';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
@@ -94,6 +126,7 @@ export default {
|
||||
snackbars,
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
amazonPaymentsModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -104,6 +137,8 @@ export default {
|
||||
oggSource: '',
|
||||
mp3Source: '',
|
||||
},
|
||||
loading: true,
|
||||
currentTipNumber: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -115,6 +150,15 @@ export default {
|
||||
castingSpell () {
|
||||
return this.$store.state.spellOptions.castingSpell;
|
||||
},
|
||||
currentTip () {
|
||||
const numberOfTips = 35 + 1;
|
||||
const min = 1;
|
||||
const randomNumber = Math.random() * (numberOfTips - min) + min;
|
||||
const tipNumber = Math.floor(randomNumber);
|
||||
this.currentTipNumber = tipNumber;
|
||||
|
||||
return this.$t(`tip${tipNumber}`);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('playSound', (sound) => {
|
||||
@@ -135,12 +179,12 @@ export default {
|
||||
// @TODO: I'm not sure these should be at the app level. Can we move these back into shop/inventory or maybe they need a lateral move?
|
||||
this.$root.$on('buyModal::showItem', (item) => {
|
||||
this.selectedItemToBuy = item;
|
||||
this.$root.$emit('show::modal', 'buy-modal');
|
||||
this.$root.$emit('bv::show::modal', 'buy-modal');
|
||||
});
|
||||
|
||||
this.$root.$on('selectMembersModal::showItem', (item) => {
|
||||
this.selectedSpellToBuy = item;
|
||||
this.$root.$emit('show::modal', 'select-member-modal');
|
||||
this.$root.$emit('bv::show::modal', 'select-member-modal');
|
||||
});
|
||||
|
||||
// @TODO split up this file, it's too big
|
||||
@@ -214,15 +258,15 @@ export default {
|
||||
}
|
||||
|
||||
// Verify the client is updated
|
||||
const serverAppVersion = response.data.appVersion;
|
||||
let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
if (isApiCall && !serverAppVersionState) {
|
||||
this.$store.state.serverAppVersion = serverAppVersion;
|
||||
} else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
if (document.activeElement.tagName !== 'INPUT' || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
location.reload(true);
|
||||
}
|
||||
}
|
||||
// const serverAppVersion = response.data.appVersion;
|
||||
// let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
// if (isApiCall && !serverAppVersionState) {
|
||||
// this.$store.state.serverAppVersion = serverAppVersion;
|
||||
// } else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
// if (document.activeElement.tagName !== 'INPUT' || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
// location.reload(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
return response;
|
||||
});
|
||||
@@ -264,7 +308,7 @@ export default {
|
||||
}
|
||||
|
||||
// Manage modals
|
||||
this.$root.$on('show::modal', (modalId, data = {}) => {
|
||||
this.$root.$on('bv::show::modal', (modalId, data = {}) => {
|
||||
if (data.fromRoot) return;
|
||||
|
||||
// Track opening of gems modal unless it's been already tracked
|
||||
@@ -286,15 +330,16 @@ export default {
|
||||
this.$store.state.modalStack.push(modalId);
|
||||
|
||||
// Hide the previous top modal
|
||||
if (modalOnTop) this.$root.$emit('hide::modal', modalOnTop, {fromRoot: true});
|
||||
if (modalOnTop) this.$root.$emit('bv::hide::modal', modalOnTop, {fromRoot: true});
|
||||
});
|
||||
|
||||
// @TODO: This part is hacky and could be solved with two options:
|
||||
// 1 - Find a way to pass fromRoot to hidden
|
||||
// 2 - Enforce that all modals use the hide::modal event
|
||||
this.$root.$on('hidden::modal', (modalId) => {
|
||||
this.$root.$on('bv::modal::hidden', (bvEvent) => {
|
||||
const modalId = bvEvent.target.id;
|
||||
|
||||
let modalStackLength = this.$store.state.modalStack.length;
|
||||
let modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
|
||||
let modalSecondToTop = this.$store.state.modalStack[modalStackLength - 2];
|
||||
// Don't remove modal if hid was called from main app
|
||||
// @TODO: I'd reather use this, but I don't know how to pass data to hidden event
|
||||
@@ -306,13 +351,20 @@ export default {
|
||||
|
||||
// Recalculate and show the last modal if there is one
|
||||
modalStackLength = this.$store.state.modalStack.length;
|
||||
modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
|
||||
if (modalOnTop) this.$root.$emit('show::modal', modalOnTop, {fromRoot: true});
|
||||
let modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
|
||||
if (modalOnTop) this.$root.$emit('bv::show::modal', modalOnTop, {fromRoot: true});
|
||||
});
|
||||
},
|
||||
mounted () {
|
||||
// Remove the index.html loading screen and now show the inapp loading
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
},
|
||||
methods: {
|
||||
resetItemToBuy ($event) {
|
||||
if (!$event) {
|
||||
// @TODO: Do we need this? I think selecting a new item
|
||||
// overwrites. @negue might know
|
||||
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
|
||||
this.selectedItemToBuy = null;
|
||||
}
|
||||
},
|
||||
@@ -330,33 +382,51 @@ export default {
|
||||
},
|
||||
customPurchase (item) {
|
||||
if (item.purchaseType === 'card') {
|
||||
if (this.user.party._id) {
|
||||
this.selectedSpellToBuy = item;
|
||||
this.selectedSpellToBuy = item;
|
||||
|
||||
this.$root.$emit('hide::modal', 'buy-modal');
|
||||
this.$root.$emit('show::modal', 'select-member-modal');
|
||||
} else {
|
||||
this.error(this.$t('errorNotInParty'));
|
||||
}
|
||||
this.$root.$emit('bv::hide::modal', 'buy-modal');
|
||||
this.$root.$emit('bv::show::modal', 'select-member-modal');
|
||||
}
|
||||
},
|
||||
async memberSelected (member) {
|
||||
this.$store.dispatch('user:castSpell', {key: this.selectedSpellToBuy.key, targetId: member.id});
|
||||
this.selectedSpellToBuy = null;
|
||||
|
||||
this.$store.dispatch('party:getMembers', {forceLoad: true});
|
||||
if (this.user.party._id) {
|
||||
this.$store.dispatch('party:getMembers', {forceLoad: true});
|
||||
}
|
||||
|
||||
this.$root.$emit('hide::modal', 'select-member-modal');
|
||||
this.$root.$emit('bv::hide::modal', 'select-member-modal');
|
||||
},
|
||||
hideLoadingScreen () {
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style src="intro.js/minified/introjs.min.css"></style>
|
||||
<style src="bootstrap/scss/bootstrap.scss" lang="scss"></style>
|
||||
<style src="assets/scss/index.scss" lang="scss"></style>
|
||||
<style src="assets/css/index.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-largeSprites-0.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-0.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-1.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-2.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-3.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-4.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-5.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-6.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-7.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-8.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-9.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-10.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-11.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-12.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-13.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-14.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-15.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-16.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-17.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-18.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-19.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-20.css"></style>
|
||||
<style src="assets/css/sprites.css"></style>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@import './sprites/*.css';
|
||||
@import './sprites.css';
|
||||
@@ -1,6 +1,24 @@
|
||||
.npc_joyful_reaper {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px 0px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
.promo_armoire_bgs_201801 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -369px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -142px -595px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.scene_calendar {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 436px;
|
||||
height: 368px;
|
||||
}
|
||||
.scene_winter_cleaning {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -142px -369px;
|
||||
width: 276px;
|
||||
height: 225px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user