mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-04 21:12:01 -05:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f269d381da | ||
|
|
3483f69559 | ||
|
|
8bbe9ac36e | ||
|
|
75b93e6ec4 | ||
|
|
f7b298d506 | ||
|
|
4bd2932955 | ||
|
|
7060f5941d | ||
|
|
21379ee357 | ||
|
|
cb46cd8eeb | ||
|
|
d9d02ca81d | ||
|
|
50c216eb41 | ||
|
|
6bdf8fdabc | ||
|
|
3db304b6bf | ||
|
|
ea85a8a3a8 | ||
|
|
e811a2700e | ||
|
|
83b573dcfc | ||
|
|
c4fa9426b3 | ||
|
|
042e5a8d63 | ||
|
|
f7ce269f3c | ||
|
|
c084f8a2b9 | ||
|
|
c1c42e17b8 | ||
|
|
306a782e7a | ||
|
|
20178c0722 | ||
|
|
256e2e809c | ||
|
|
592345e22c | ||
|
|
292b2acb1e | ||
|
|
977f9d5174 | ||
|
|
85644fdc1b | ||
|
|
138b5c4bdb | ||
|
|
52edb8a8da | ||
|
|
1999e1098e | ||
|
|
4d3a0c0571 | ||
|
|
706de95458 | ||
|
|
e3c1eaa9d2 | ||
|
|
17c0f795cc | ||
|
|
cb5ac9014e | ||
|
|
0be681b7a2 | ||
|
|
5360f9e587 | ||
|
|
4553a411f6 | ||
|
|
613f51b08d | ||
|
|
2292ba2694 | ||
|
|
befacca457 | ||
|
|
5cd30b430d | ||
|
|
c5d9ee1e0a | ||
|
|
234328f2ba | ||
|
|
029afa197e | ||
|
|
05b35c5147 | ||
|
|
c9427ad34c | ||
|
|
d6c62262f1 | ||
|
|
ec1d378504 | ||
|
|
3bb88f450a | ||
|
|
97a38e68c5 | ||
|
|
be948a1bf2 | ||
|
|
018976a723 | ||
|
|
00e5896ac6 | ||
|
|
36bc693545 | ||
|
|
f27706cb4b | ||
|
|
f6f99ec57e | ||
|
|
c852d9d581 | ||
|
|
4a78514308 | ||
|
|
265b48752d | ||
|
|
db0b0d6b6d | ||
|
|
20f1087552 | ||
|
|
2e9bc2c31c | ||
|
|
b606dd1c40 | ||
|
|
db1c2fd5a2 | ||
|
|
de1e477ce2 | ||
|
|
67318177a2 | ||
|
|
cd1be828ca | ||
|
|
9ffebc10a7 | ||
|
|
5cd11ed343 | ||
|
|
9de118f0d9 | ||
|
|
5fbec4069e | ||
|
|
a0f10cbf4b | ||
|
|
0e069e78d5 | ||
|
|
dea847ba1a | ||
|
|
46ed1813c6 | ||
|
|
e9750353a7 | ||
|
|
05ea2c1ce6 | ||
|
|
74d1c7763e | ||
|
|
6f034bb5dd | ||
|
|
aeb8d4f500 | ||
|
|
58d910fe62 | ||
|
|
71f2f31606 | ||
|
|
feae40cf0a | ||
|
|
0d3fe53155 | ||
|
|
c3a3c1514a | ||
|
|
cc532fa993 | ||
|
|
ba66a1c098 | ||
|
|
b4d5c634b3 | ||
|
|
216006beab | ||
|
|
7c236e7e0e | ||
|
|
0221d2d7f9 | ||
|
|
567eb1d98b | ||
|
|
3cf533f261 | ||
|
|
c30c51f386 | ||
|
|
2de794c32b | ||
|
|
7d000d2cf6 | ||
|
|
280b720c13 | ||
|
|
9e1f7f3811 | ||
|
|
fefaed368a | ||
|
|
6f370395ea | ||
|
|
65566f7607 | ||
|
|
c0117706e4 | ||
|
|
f267456a30 | ||
|
|
228b724d52 | ||
|
|
19b09b4894 | ||
|
|
f49d21d7b4 | ||
|
|
c08c0685f3 | ||
|
|
59bfe66c94 | ||
|
|
023fd6e6b0 | ||
|
|
7ee2f90f37 | ||
|
|
46709ddadd | ||
|
|
a3ee09e764 | ||
|
|
4127f36c02 | ||
|
|
dad924ac7d | ||
|
|
547c87dee7 | ||
|
|
99a2013767 | ||
|
|
a5e0e171cc | ||
|
|
1d93943458 | ||
|
|
7f2719a75c | ||
|
|
8a9ed04f5e | ||
|
|
e6f605f23a | ||
|
|
0af1203832 | ||
|
|
1de379a2c3 | ||
|
|
388861b503 | ||
|
|
f52806ed69 | ||
|
|
d89b9e08af | ||
|
|
f8a99bd127 | ||
|
|
a82b60f144 | ||
|
|
65e71140ee | ||
|
|
f192ca4c6f | ||
|
|
36b09d40b9 | ||
|
|
1292f9a3d5 | ||
|
|
e7418472f6 | ||
|
|
850f332ddc | ||
|
|
59fb32ea2e | ||
|
|
727cdc9402 | ||
|
|
638c9dee89 | ||
|
|
2adac35e31 | ||
|
|
15d4f7d6ab | ||
|
|
76222ac344 | ||
|
|
2659a4117b | ||
|
|
a0ee73e944 | ||
|
|
6174624b89 | ||
|
|
c8b6e8ea7c | ||
|
|
409b5d5965 | ||
|
|
e7209511ca | ||
|
|
8c76ccd39b | ||
|
|
399c91ccab | ||
|
|
4bbebdd237 | ||
|
|
0fd85c0d60 | ||
|
|
8c68f450c6 |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,6 @@
|
||||
# Reporting Bugs
|
||||
|
||||
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitrpg/issues/2760)
|
||||
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
# Pull Request
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
|
||||
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
||||
### General Info
|
||||
|
||||
@@ -12,6 +12,8 @@ before_install:
|
||||
- $CXX --version
|
||||
- npm install -g npm@4
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
install:
|
||||
- npm install &> npm.install.log || (cat npm.install.log; false)
|
||||
before_script:
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
@@ -31,3 +33,4 @@ env:
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="test:karma" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
- TEST="apidoc"
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,43 +1,16 @@
|
||||
FROM ubuntu:trusty
|
||||
|
||||
MAINTAINER Sabe Jones <sabe@habitica.com>
|
||||
|
||||
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
|
||||
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
|
||||
|
||||
# Install prerequisites
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
git \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
libkrb5-dev \
|
||||
python
|
||||
|
||||
# Install NodeJS
|
||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Install npm@latest
|
||||
RUN curl -sL https://www.npmjs.org/install.sh | sh
|
||||
|
||||
# Clean up package management
|
||||
RUN apt-get clean
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
FROM node:boron
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp grunt-cli bower mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
WORKDIR /habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN bower install --allow-root
|
||||
|
||||
# Create environment config file and build directory
|
||||
RUN cp config.json.example config.json
|
||||
# Create Build dir
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
|
||||
@@ -5,8 +5,7 @@ Habitica [ - "Coders (Web & Mobile)" section.
|
||||
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
|
||||
|
||||
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
|
||||
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
|
||||
|
||||
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
|
||||
|
||||
@@ -3,7 +3,7 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
|
||||
|
||||
/**
|
||||
* database_reports/count_users_who_own_specified_gear.js
|
||||
* https://github.com/HabitRPG/habitrpg/pull/3884
|
||||
* https://github.com/HabitRPG/habitica/pull/3884
|
||||
*/
|
||||
|
||||
var thingsOfInterest = {
|
||||
|
||||
36
gulp/gulp-bootstrap.js
Normal file
36
gulp/gulp-bootstrap.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// Copy Bootstrap 4 config variables from /website /node_modules so we can check
|
||||
// them into Git
|
||||
|
||||
const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.scss';
|
||||
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
|
||||
|
||||
// https://stackoverflow.com/a/14387791/969528
|
||||
function copyFile(source, target, cb) {
|
||||
let cbCalled = false;
|
||||
|
||||
function done(err) {
|
||||
if (!cbCalled) {
|
||||
cb(err);
|
||||
cbCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
let rd = fs.createReadStream(source);
|
||||
rd.on('error', done);
|
||||
let wr = fs.createWriteStream(target);
|
||||
wr.on('error', done);
|
||||
wr.on('close', () => done());
|
||||
rd.pipe(wr);
|
||||
}
|
||||
|
||||
gulp.task('bootstrap', (done) => {
|
||||
// use new config
|
||||
copyFile(
|
||||
BOOSTRAP_NEW_CONFIG_PATH,
|
||||
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
|
||||
done,
|
||||
);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
|
||||
});
|
||||
|
||||
if (numberOfSheetsThatAreTooBig > 0) {
|
||||
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitrpg/pull/6683#issuecomment-185462180
|
||||
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
'use strict';
|
||||
|
||||
/****************************************
|
||||
* Author: @Alys
|
||||
*
|
||||
* Reason: Collection quests are being changed
|
||||
* to require fewer items collected:
|
||||
* https://github.com/HabitRPG/habitrpg/pull/7987
|
||||
* This will cause existing quests to end sooner
|
||||
* than the party is expecting.
|
||||
* This script inserts an explanatory `system`
|
||||
* message into the chat for affected parties.
|
||||
***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const uuid = require('uuid');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
const connectToDb = require('./utils/connect').connectToDb;
|
||||
const closeDb = require('./utils/connect').closeDb;
|
||||
|
||||
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.wikia.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
|
||||
|
||||
const timer = new Timer();
|
||||
|
||||
// PROD: Enable prod db
|
||||
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
|
||||
const DB_URI = 'mongodb://localhost/habitrpg';
|
||||
|
||||
const COLLECTION_QUESTS = [
|
||||
'vice2',
|
||||
'egg',
|
||||
'moonstone1',
|
||||
'goldenknight1',
|
||||
'dilatoryDistress1',
|
||||
];
|
||||
|
||||
let Groups;
|
||||
|
||||
connectToDb(DB_URI).then((db) => {
|
||||
Groups = db.collection('groups');
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(findPartiesWithCollectionQuest)
|
||||
// .then(displayGroups) // for testing only
|
||||
.then(addMessageToGroups)
|
||||
.then(() => {
|
||||
timer.stop();
|
||||
closeDb();
|
||||
}).catch(reportError);
|
||||
|
||||
function reportError (err) {
|
||||
logger.error('Uh oh, an error occurred');
|
||||
closeDb();
|
||||
timer.stop();
|
||||
throw err;
|
||||
}
|
||||
|
||||
function findPartiesWithCollectionQuest () {
|
||||
logger.info('Looking up groups on collection quests...');
|
||||
|
||||
return Groups.find({'quest.key': {$in: COLLECTION_QUESTS}}, ['name','quest']).toArray().then((groups) => {
|
||||
logger.success('Found', groups.length, 'parties on collection quests');
|
||||
|
||||
return Promise.resolve(groups);
|
||||
})
|
||||
}
|
||||
|
||||
function displayGroups (groups) { // for testing only
|
||||
logger.info('Displaying parties...');
|
||||
console.log(groups);
|
||||
return Promise.resolve(groups);
|
||||
}
|
||||
|
||||
function updateGroupById (group) {
|
||||
var newMessage = {
|
||||
'id' : uuid.v4(),
|
||||
'text' : message,
|
||||
'timestamp': Date.now(),
|
||||
'likes': {},
|
||||
'flags': {},
|
||||
'flagCount': 0,
|
||||
'uuid': 'system'
|
||||
};
|
||||
return Groups.findOneAndUpdate({_id: group._id}, {$push:{"chat" :{$each: [newMessage], $position:0}}}, {returnOriginal: false});
|
||||
// Does not set the newMessage flag for all party members because I don't think it's essential and
|
||||
// I don't want to run the extra code (extra database load, extra opportunity for bugs).
|
||||
}
|
||||
|
||||
function addMessageToGroups (groups) {
|
||||
let queue = new TaskQueue(Promise, 300);
|
||||
|
||||
logger.info('About to update', groups.length, 'parties...');
|
||||
|
||||
return Promise.map(groups, queue.wrap(updateGroupById)).then((result) => {
|
||||
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
|
||||
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
|
||||
|
||||
logger.success(updates.length, 'parties have been notified');
|
||||
|
||||
if (failures.length > 0) {
|
||||
logger.error(failures.length, 'parties could not be notified');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
114
migrations/20170616_achievements.js
Normal file
114
migrations/20170616_achievements.js
Normal file
@@ -0,0 +1,114 @@
|
||||
var migrationName = '20170616_achievements';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Updates to achievements for June 16, 2017 biweekly merge
|
||||
* 1. Multiply various collection quest achievements based on difficulty reduction
|
||||
* 2. Award Joined Challenge achievement to those who should have it already
|
||||
*/
|
||||
|
||||
import monk from 'monk';
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
$or: [
|
||||
{'achievements.quests.dilatoryDistress1': {$gt:0}},
|
||||
{'achievements.quests.egg': {$gt:0}},
|
||||
{'achievements.quests.goldenknight1': {$gt:0}},
|
||||
{'achievements.quests.moonstone1': {$gt:0}},
|
||||
{'achievements.quests.vice2': {$gt:0}},
|
||||
{'achievements.challenges': {$exists: true, $ne: []}},
|
||||
{'challenges': {$exists: true, $ne: []}},
|
||||
],
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
'achievements',
|
||||
'challenges',
|
||||
],
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
var set = {'migration': migrationName};
|
||||
|
||||
if (user.challenges.length > 0 || user.achievements.challenges.length > 0) {
|
||||
set['achievements.joinedChallenge'] = true;
|
||||
}
|
||||
if (user.achievements.quests.dilatoryDistress1) {
|
||||
set['achievements.quests.dilatoryDistress1'] = Math.ceil(user.achievements.quests.dilatoryDistress1 * 1.25);
|
||||
}
|
||||
if (user.achievements.quests.egg) {
|
||||
set['achievements.quests.egg'] = Math.ceil(user.achievements.quests.egg * 2.5);
|
||||
}
|
||||
if (user.achievements.quests.goldenknight1) {
|
||||
set['achievements.quests.goldenknight1'] = user.achievements.quests.goldenknight1 * 5;
|
||||
}
|
||||
if (user.achievements.quests.moonstone1) {
|
||||
set['achievements.quests.moonstone1'] = user.achievements.quests.moonstone1 * 5;
|
||||
}
|
||||
if (user.achievements.quests.vice2) {
|
||||
set['achievements.quests.vice2'] = Math.ceil(user.achievements.quests.vice2 * 1.5);
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['back_mystery_201704','armor_mystery_201704']
|
||||
$each:['body_mystery_201705','head_mystery_201705']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
2421
npm-shrinkwrap.json
generated
2421
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.91.0",
|
||||
"version": "3.97.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -13,10 +13,12 @@
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"axios": "^0.15.3",
|
||||
"axios": "^0.16.0",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
@@ -29,13 +31,14 @@
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "^4.0.0-alpha.6",
|
||||
"bootstrap-vue": "^0.15.8",
|
||||
"bower": "~1.3.12",
|
||||
"browserify": "~12.0.1",
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.26.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
@@ -96,7 +99,7 @@
|
||||
"postcss-easy-import": "^2.0.0",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta11",
|
||||
"pug": "^2.0.0-beta.12",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.74.0",
|
||||
@@ -108,6 +111,9 @@
|
||||
"shelljs": "^0.7.6",
|
||||
"stripe": "^4.2.0",
|
||||
"superagent": "^3.4.3",
|
||||
"svg-inline-loader": "^0.7.1",
|
||||
"svg-url-loader": "^2.0.2",
|
||||
"svgo-loader": "^1.2.1",
|
||||
"universal-analytics": "~0.3.2",
|
||||
"url-loader": "^0.5.7",
|
||||
"useragent": "^2.1.9",
|
||||
@@ -119,10 +125,10 @@
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-style-loader": "^2.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^2.6.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.1.0",
|
||||
"winston-loggly-bulk": "^1.4.2",
|
||||
"xml2js": "^0.4.4"
|
||||
@@ -134,7 +140,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit",
|
||||
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
@@ -151,14 +157,15 @@
|
||||
"test:nodemon": "gulp test:nodemon",
|
||||
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "node webpack/build.js",
|
||||
"client:dev": "gulp bootstrap && node webpack/dev-server.js",
|
||||
"client:build": "gulp bootstrap && node webpack/build.js",
|
||||
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
|
||||
"client:e2e": "node test/client/e2e/runner.js",
|
||||
"client:test": "npm run client:unit && npm run client:e2e",
|
||||
"start": "gulp run:dev",
|
||||
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
|
||||
"postinstall": "bower --config.interactive=false install -f && gulp build && npm run client:build",
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
@@ -168,7 +175,7 @@
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"cross-env": "^3.1.4",
|
||||
"cross-env": "^4.0.0",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
@@ -192,7 +199,7 @@
|
||||
"karma-mocha": "^0.2.0",
|
||||
"karma-mocha-reporter": "^1.1.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-sinon-chai": "^1.2.0",
|
||||
"karma-sinon-chai": "~1.2.0",
|
||||
"karma-sinon-stub-promise": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.24",
|
||||
@@ -206,6 +213,7 @@
|
||||
"nightwatch": "^0.9.12",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"require-again": "^2.0.0",
|
||||
"rewire": "^2.3.3",
|
||||
"selenium-server": "^3.0.1",
|
||||
|
||||
1
test/README.md
Normal file
1
test/README.md
Normal file
@@ -0,0 +1 @@
|
||||
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).
|
||||
@@ -1,123 +0,0 @@
|
||||
# So you want to write API integration tests?
|
||||
|
||||
@TODO rewrite
|
||||
|
||||
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
|
||||
|
||||
## What is this?
|
||||
|
||||
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
|
||||
|
||||
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
|
||||
|
||||
## How to run the tests
|
||||
|
||||
First, install gulp.
|
||||
|
||||
```bash
|
||||
$ npm install -g gulp
|
||||
```
|
||||
|
||||
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
|
||||
|
||||
```bash
|
||||
$ gulp test:api-v2
|
||||
```
|
||||
|
||||
It may take a little while to get going, since it requires the web server to start up before the tests can run.
|
||||
|
||||
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
|
||||
|
||||
```bash
|
||||
$ gulp test:api-v2:watch
|
||||
```
|
||||
|
||||
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
|
||||
|
||||
```bash
|
||||
$ gulp test:api:safe
|
||||
```
|
||||
|
||||
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
|
||||
|
||||
```bash
|
||||
$ gulp test:nodemon
|
||||
```
|
||||
|
||||
And run your tests in another pane:
|
||||
|
||||
```bash
|
||||
$ mocha path/to/file.js
|
||||
|
||||
# Mark a test with the `.only` attribute
|
||||
$ mocha
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
|
||||
|
||||
Each test should:
|
||||
|
||||
* encompase a single route
|
||||
* begin with the REST parameter for the route
|
||||
* display the full name of the route, swapping out `/` for `_`
|
||||
* end with test.js
|
||||
|
||||
So, for the `POST` route `/groups/:id/leave`, it would be
|
||||
|
||||
```bash
|
||||
POST-groups_id_leave.test.js
|
||||
```
|
||||
|
||||
## Promises
|
||||
|
||||
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
|
||||
|
||||
```js
|
||||
it('does something', () => {
|
||||
let user;
|
||||
|
||||
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
|
||||
user = _user;
|
||||
|
||||
return user.post('/groups', {
|
||||
type: 'party',
|
||||
});
|
||||
}).then((party) => { // the result of the promise above is the argument of the function
|
||||
return user.put(`/groups/${party._id}`, {
|
||||
name: 'My party',
|
||||
});
|
||||
}).then((result) => {
|
||||
return user.get('/groups/party');
|
||||
}).then((party) => {
|
||||
expect(party.name).to.eql('My party');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
|
||||
|
||||
```js
|
||||
it('makes the party creator the leader automatically', () => {
|
||||
return expect(user.post('/groups', {
|
||||
type: 'party',
|
||||
})).to.eventually.have.deep.property('leader._id', user._id);
|
||||
});
|
||||
```
|
||||
|
||||
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
|
||||
|
||||
```js
|
||||
it('returns an error', () => {
|
||||
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
text: t('messageGroupNotFound'),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!
|
||||
@@ -1,4 +0,0 @@
|
||||
# How to run tests:
|
||||
|
||||
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
|
||||
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other
|
||||
@@ -304,5 +304,15 @@ describe('POST /challenges', () => {
|
||||
|
||||
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
|
||||
});
|
||||
|
||||
it('awards achievement if this is creator\'s first challenge', async () => {
|
||||
await groupLeader.post('/challenges', {
|
||||
group: group._id,
|
||||
name: 'Test Challenge',
|
||||
shortName: 'TC Label',
|
||||
});
|
||||
groupLeader = await groupLeader.sync();
|
||||
expect(groupLeader.achievements.joinedChallenge).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,5 +123,12 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
|
||||
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
|
||||
});
|
||||
|
||||
it('awards achievement if this is user\'s first challenge', async () => {
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await authorizedUser.sync();
|
||||
expect(authorizedUser.achievements.joinedChallenge).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,8 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
||||
await sleep(0.5);
|
||||
|
||||
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
|
||||
expect(winningUser.notifications.length).to.equal(1);
|
||||
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
|
||||
expect(winningUser.notifications.length).to.equal(2); // 2 because winningUser just joined the challenge, which now awards an achievement
|
||||
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
|
||||
});
|
||||
|
||||
it('gives winner gems as reward', async () => {
|
||||
|
||||
@@ -84,6 +84,10 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
});
|
||||
await user.post(`/groups/${privateGroup._id}/invite`, {
|
||||
uuids: [anotherUser._id],
|
||||
});
|
||||
await anotherUser.post(`/groups/${privateGroup._id}/join`);
|
||||
let { message } = await user.post(`/groups/${privateGroup._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
let flagResult = await admin.post(`/groups/${privateGroup._id}/chat/${message.id}/flag`);
|
||||
@@ -91,7 +95,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
expect(flagResult.flags[admin._id]).to.equal(true);
|
||||
expect(flagResult.flagCount).to.equal(5);
|
||||
|
||||
let groupWithFlags = await user.get(`/groups/${privateGroup._id}`);
|
||||
let groupWithFlags = await anotherUser.get(`/groups/${privateGroup._id}`);
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(messageToCheck).to.not.exist;
|
||||
@@ -125,4 +129,20 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
message: t('messageGroupChatFlagAlreadyReported'),
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a hidden message to the original poster', async () => {
|
||||
let { message } = await user.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
|
||||
|
||||
await admin.post(`/groups/${group._id}/chat/${message.id}/flag`);
|
||||
|
||||
let groupWithFlags = await user.get(`/groups/${group._id}`);
|
||||
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(messageToCheck).to.exist;
|
||||
|
||||
let auGroupWithFlags = await anotherUser.get(`/groups/${group._id}`);
|
||||
let auMessageToCheck = find(auGroupWithFlags.chat, {id: message.id});
|
||||
|
||||
expect(auMessageToCheck).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,9 +70,9 @@ describe('POST /chat', () => {
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Your chat privileges have been revoked.',
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -74,6 +74,18 @@ describe('POST /group', () => {
|
||||
expect(updatedUser.guilds).to.include(guild._id);
|
||||
});
|
||||
|
||||
it('awards the Joined Guild achievement', async () => {
|
||||
await user.post('/groups', {
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
|
||||
let updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.achievements.joinedGuild).to.eql(true);
|
||||
});
|
||||
|
||||
context('public guild', () => {
|
||||
it('creates a group', async () => {
|
||||
let groupName = 'Test Public Guild';
|
||||
|
||||
@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
|
||||
|
||||
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await joiningUser.post(`/groups/${publicGuild._id}/join`);
|
||||
|
||||
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a private guild', () => {
|
||||
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[1].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
let guild;
|
||||
let member;
|
||||
let member2;
|
||||
let adminUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
|
||||
@@ -28,6 +29,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
invitedUser = invitees[0];
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
});
|
||||
|
||||
context('All Groups', () => {
|
||||
@@ -42,7 +44,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when user is a non-leader member of a group', async () => {
|
||||
it('returns an error when user is a non-leader member of a group and not an admin', async () => {
|
||||
expect(member2.post(`/groups/${guild._id}/removeMember/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
@@ -87,7 +89,30 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
|
||||
let invitedUserWithoutInvite = await invitedUser.get('/user');
|
||||
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
|
||||
});
|
||||
|
||||
it('allows an admin to remove other members', async () => {
|
||||
await adminUser.post(`/groups/${guild._id}/removeMember/${member._id}`);
|
||||
let memberRemoved = await member.get('/user');
|
||||
|
||||
expect(memberRemoved.guilds.indexOf(guild._id)).eql(-1);
|
||||
});
|
||||
|
||||
it('allows an admin to remove other invites', async () => {
|
||||
await adminUser.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
|
||||
|
||||
let invitedUserWithoutInvite = await invitedUser.get('/user');
|
||||
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
|
||||
});
|
||||
|
||||
it('does not allow an admin to remove a leader', async () => {
|
||||
expect(adminUser.post(`/groups/${guild._id}/removeMember/${leader._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: t('cannotRemoveCurrentLeader'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('PUT /group', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let leader, nonLeader, groupToUpdate, adminUser;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let groupUpdatedName = 'Test Public Guild Updated';
|
||||
@@ -18,13 +19,13 @@ describe('PUT /group', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to update', async () => {
|
||||
it('returns an error when a user that is not an admin or group leader tries to update', async () => {
|
||||
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
@@ -44,6 +45,15 @@ describe('PUT /group', () => {
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
|
||||
it('allows an admin to update a guild', async () => {
|
||||
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
});
|
||||
expect(updatedGroup.leader._id).to.eql(leader._id);
|
||||
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
|
||||
expect(updatedGroup.name).to.equal(groupUpdatedName);
|
||||
});
|
||||
|
||||
it('allows a leader to change leaders', async () => {
|
||||
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /members/:toUserId/objections/:interaction', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(
|
||||
user.get('/members/invalidUUID/objections/send-private-message')
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(
|
||||
user.get(`/members/${dummyId}/objections/send-private-message`)
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', {userId: dummyId}),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing interactions', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/hug-a-whole-forest-of-trees`)
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array if there are no objections', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/send-private-message`)
|
||||
).to.eventually.be.fulfilled.and.eql([]);
|
||||
});
|
||||
|
||||
it('returns an array of objections if any exist', async () => {
|
||||
let receiver = await generateUser({'inbox.blocks': [user._id]});
|
||||
|
||||
await expect(
|
||||
user.get(`/members/${receiver._id}/objections/send-private-message`)
|
||||
).to.eventually.be.fulfilled.and.eql([
|
||||
t('notAuthorizedToSendMessageToThisUser'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -82,6 +82,20 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked', async () => {
|
||||
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
let receiver = await generateUser();
|
||||
|
||||
await expect(userWithChatRevoked.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a private message to a user', async () => {
|
||||
let receiver = await generateUser();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user is not found', async () => {
|
||||
it('returns error when recipient is not found', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
@@ -55,7 +55,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user attempts to send gems to themselves', async () => {
|
||||
it('returns error when user attempts to send gems to themselves', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
@@ -67,6 +67,64 @@ describe('POST /members/transfer-gems', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when recipient has blocked the sender', async () => {
|
||||
let receiverWhoBlocksUser = await generateUser({'inbox.blocks': [userToSendMessage._id]});
|
||||
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiverWhoBlocksUser._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notAuthorizedToSendMessageToThisUser'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when sender has blocked recipient', async () => {
|
||||
let sender = await generateUser({'inbox.blocks': [receiver._id]});
|
||||
|
||||
await expect(sender.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notAuthorizedToSendMessageToThisUser'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when chat privileges are revoked', async () => {
|
||||
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
|
||||
await expect(userWithChatRevoked.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works when only the recipient\'s chat privileges are revoked', async () => {
|
||||
let receiverWithChatRevoked = await generateUser({'flags.chatRevoked': true});
|
||||
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
gemAmount,
|
||||
toUserId: receiverWithChatRevoked._id,
|
||||
})).to.eventually.be.fulfilled;
|
||||
|
||||
let updatedReceiver = await receiverWithChatRevoked.get('/user');
|
||||
let updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
expect(updatedReceiver.balance).to.equal(gemAmount / 4);
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns error when there is no gemAmount', async () => {
|
||||
await expect(userToSendMessage.post('/members/transfer-gems', {
|
||||
message,
|
||||
@@ -144,7 +202,7 @@ describe('POST /members/transfer-gems', () => {
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not requrie a message', async () => {
|
||||
it('does not require a message', async () => {
|
||||
await userToSendMessage.post('/members/transfer-gems', {
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
|
||||
25
test/api/v3/integration/shops/GET-shops_backgrounds.test.js
Normal file
25
test/api/v3/integration/shops/GET-shops_backgrounds.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /shops/backgrounds', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns a valid shop object', async () => {
|
||||
let shop = await user.get('/shops/backgrounds');
|
||||
expect(shop.identifier).to.equal('backgroundShop');
|
||||
expect(shop.text).to.eql(t('backgroundShop'));
|
||||
expect(shop.notes).to.eql(t('backgroundShopText'));
|
||||
expect(shop.imageName).to.equal('background_shop');
|
||||
expect(shop.sets).to.be.an('array');
|
||||
|
||||
let sets = shop.sets.map(set => set.identifier);
|
||||
expect(sets).to.include('incentiveBackgrounds');
|
||||
expect(sets).to.include('backgrounds062014');
|
||||
});
|
||||
});
|
||||
@@ -208,6 +208,20 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(task.completed).to.equal(false);
|
||||
});
|
||||
|
||||
it('computes isDue', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.isDue).to.equal(true);
|
||||
});
|
||||
|
||||
it('computes nextDue', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('scores up daily even if it is already completed'); // Yes?
|
||||
|
||||
it('scores down daily even if it is already uncompleted'); // Yes?
|
||||
|
||||
@@ -509,6 +509,8 @@ describe('POST /tasks/user', () => {
|
||||
expect(task.daysOfMonth).to.eql([15]);
|
||||
expect(task.weeksOfMonth).to.eql([3]);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
expect(task.isDue).to.be.true;
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('creates multiple dailys', async () => {
|
||||
@@ -613,6 +615,18 @@ describe('POST /tasks/user', () => {
|
||||
expect((new Date(task.startDate)).getDay()).to.eql(today);
|
||||
});
|
||||
|
||||
it('returns an error if the start date is empty', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
startDate: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('can create checklists', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
@@ -395,12 +396,15 @@ describe('PUT /tasks/:id', () => {
|
||||
notes: 'some new notes',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: moment().add(1, 'days').toDate(),
|
||||
});
|
||||
|
||||
expect(savedDaily.text).to.eql('some new text');
|
||||
expect(savedDaily.notes).to.eql('some new notes');
|
||||
expect(savedDaily.frequency).to.eql('daily');
|
||||
expect(savedDaily.everyX).to.eql(5);
|
||||
expect(savedDaily.isDue).to.be.false;
|
||||
expect(savedDaily.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('can update checklists (replace it)', async () => {
|
||||
|
||||
@@ -112,8 +112,8 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(member2.notifications.length).to.equal(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -52,14 +52,14 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
@@ -82,14 +82,14 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('DELETE /user', () => {
|
||||
let user;
|
||||
@@ -25,7 +26,7 @@ describe('DELETE /user', () => {
|
||||
user = await generateUser({balance: 10});
|
||||
});
|
||||
|
||||
it('returns an errors if password is wrong', async () => {
|
||||
it('returns an error if password is wrong', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: 'wrong-password',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
@@ -35,6 +36,33 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if password is not supplied', async () => {
|
||||
await expect(user.del('/user', {
|
||||
password: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
let feedbackText = 'spam feedback ';
|
||||
let feedback = feedbackText;
|
||||
while (feedback.length < 10000) {
|
||||
feedback = feedback + feedbackText;
|
||||
}
|
||||
|
||||
await expect(user.del('/user', {
|
||||
password,
|
||||
feedback,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if user has active subscription', async () => {
|
||||
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
|
||||
|
||||
@@ -96,6 +124,32 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
let feedback = 'Reasons for Deletion';
|
||||
await user.del('/user', {
|
||||
password,
|
||||
feedback,
|
||||
});
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('does not send email if no feedback is supplied', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
expect(email.sendTxn).to.not.be.called;
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
|
||||
@@ -221,6 +221,27 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(syncedGroupTask.value).to.equal(0);
|
||||
});
|
||||
|
||||
it('increases both user\'s achievement values', async () => {
|
||||
let party = await createAndPopulateGroup({
|
||||
members: 1,
|
||||
});
|
||||
let leader = party.groupLeader;
|
||||
let recipient = party.members[0];
|
||||
await leader.update({'stats.gp': 10});
|
||||
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
|
||||
await leader.sync();
|
||||
await recipient.sync();
|
||||
expect(leader.achievements.birthday).to.equal(1);
|
||||
expect(recipient.achievements.birthday).to.equal(1);
|
||||
});
|
||||
|
||||
it('only increases user\'s achievement one if target == caster', async () => {
|
||||
await user.update({'stats.gp': 10});
|
||||
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
|
||||
await user.sync();
|
||||
expect(user.achievements.birthday).to.equal(1);
|
||||
});
|
||||
|
||||
// TODO find a way to have sinon working in integration tests
|
||||
// it doesn't work when tests are running separately from server
|
||||
it('passes correct target to spell when targetType === \'task\'');
|
||||
|
||||
@@ -251,7 +251,6 @@ describe('POST /user/auth/local/register', () => {
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -525,6 +525,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
@@ -555,6 +556,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
@@ -593,6 +595,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
@@ -623,6 +626,7 @@ describe('Amazon Payments', () => {
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
@@ -313,6 +313,24 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
expect(user.stats.hp).to.equal(healthBefore);
|
||||
});
|
||||
|
||||
it('sets isDue for daily', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].isDue).to.be.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos', () => {
|
||||
@@ -358,6 +376,22 @@ describe('cron', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('computes isDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
it('computes nextDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
@@ -504,6 +505,18 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
user.purchased.plan.extraMonths = 5;
|
||||
|
||||
@@ -653,5 +666,32 @@ describe('payments/index', () => {
|
||||
|
||||
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
|
||||
let planExpirationDate = new Date();
|
||||
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
|
||||
let mysteryItem = 'item';
|
||||
let mysteryItems = [mysteryItem];
|
||||
let consecutive = {
|
||||
trinkets: 3,
|
||||
};
|
||||
|
||||
// set expired plan with unused items
|
||||
plan.mysteryItems = mysteryItems;
|
||||
plan.consecutive = consecutive;
|
||||
plan.dateCreated = planExpirationDate;
|
||||
plan.dateTerminated = planExpirationDate;
|
||||
plan.customerId = null;
|
||||
|
||||
user.purchased.plan = plan;
|
||||
|
||||
await user.save();
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
|
||||
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('Canceling a subscription for group', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents non group leader from manging subscription', async () => {
|
||||
it('prevents non group leader from managing subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import stripeModule from 'stripe';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import * as sender from '../../../../../../../website/server/libs/email';
|
||||
import * as api from '../../../../../../../website/server/libs/payments';
|
||||
@@ -12,17 +13,24 @@ import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('Purchasing a subscription for group', () => {
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
|
||||
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
|
||||
|
||||
let plan, group, user, data;
|
||||
let stripe = stripeModule('test');
|
||||
let groupLeaderName = 'sender';
|
||||
let groupName = 'test group';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.profile.name = groupLeaderName;
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
@@ -81,7 +89,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
sender.sendTxn.restore();
|
||||
});
|
||||
|
||||
it('creates a subscription', async () => {
|
||||
it('creates a group plan', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
@@ -157,7 +165,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
|
||||
});
|
||||
|
||||
it('sends an email to members of group', async () => {
|
||||
it('sends an email to member of group who was not a subscriber', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.guilds.push(group._id);
|
||||
@@ -169,11 +177,181 @@ describe('Purchasing a subscription for group', () => {
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
|
||||
]);
|
||||
// confirm that the other email sent is appropriate:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.key = 'basic_earned';
|
||||
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
|
||||
sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
});
|
||||
|
||||
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: { // eslint-disable-line camelcase
|
||||
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
|
||||
cycles_completed: 1, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
plan.planId = 'basic_earned';
|
||||
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledTwice;
|
||||
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.firstCall.args[2]).to.eql([
|
||||
{name: 'LEADER', content: user.profile.name},
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
|
||||
]);
|
||||
// confirm that the other email sent is not a cancel-subscription email:
|
||||
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
|
||||
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledFourTimes;
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
let recipient = new User();
|
||||
recipient.profile.name = 'recipient';
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.guilds.push(group._id);
|
||||
await recipient.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledFourTimes;
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
{name: 'LEADER', content: groupLeaderName},
|
||||
{name: 'GROUP_NAME', content: groupName},
|
||||
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
@@ -333,7 +511,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Android)');
|
||||
it('adds months to members with existing recurring subscription (iOs)');
|
||||
it('adds months to members with existing recurring subscription (iOS)');
|
||||
|
||||
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
|
||||
let recipient = new User();
|
||||
@@ -603,7 +781,7 @@ describe('Purchasing a subscription for group', () => {
|
||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not modify a user with a Google subscription', async () => {
|
||||
it('does not modify a user with an Android subscription', async () => {
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
|
||||
@@ -447,6 +447,7 @@ describe('Paypal Payments', () => {
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,6 +465,7 @@ describe('Paypal Payments', () => {
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -683,6 +683,7 @@ describe('Stripe Payments', () => {
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -702,6 +703,7 @@ describe('Stripe Payments', () => {
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('applies damage only to participating members of party even under buggy conditions', async () => {
|
||||
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitrpg/issues/7653
|
||||
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitica/issues/7653
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
|
||||
@@ -112,6 +112,41 @@ describe('Groups Controller', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAbleToEditGroup', () => {
|
||||
var guild;
|
||||
|
||||
beforeEach(() => {
|
||||
user.contributor = {};
|
||||
guild = specHelper.newGroup({
|
||||
_id: 'unique-guild-id',
|
||||
type: 'guild',
|
||||
members: ['not-user-id'],
|
||||
$save: sandbox.spy(),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true if user is an admin', () => {
|
||||
guild.leader = 'not-user-id';
|
||||
user.contributor.admin = true;
|
||||
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
|
||||
});
|
||||
|
||||
it('returns true if user is group leader', () => {
|
||||
guild.leader = {_id: user._id}
|
||||
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
|
||||
});
|
||||
|
||||
it('returns false is user is not a leader or admin', () => {
|
||||
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('returns false is user is an admin but group is a party', () => {
|
||||
guild.type = 'party';
|
||||
user.contributor.admin = true;
|
||||
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('editGroup', () => {
|
||||
var guild;
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
describe('closeMenu Directive', function() {
|
||||
var scope;
|
||||
|
||||
beforeEach(module('habitrpg'));
|
||||
|
||||
beforeEach(inject(function($rootScope) {
|
||||
scope = $rootScope.$new();
|
||||
|
||||
scope.$digest();
|
||||
}));
|
||||
|
||||
it('closes a connected menu when element is clicked', inject(function($compile) {
|
||||
var menuElement = $compile('<a data-close-menu menu="mobile">')(scope);
|
||||
scope._expandedMenu = { menu: 'mobile' };
|
||||
|
||||
menuElement.appendTo(document.body);
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
}));
|
||||
|
||||
it('closes a connected menu when child element is clicked', inject(function($compile) {
|
||||
var menuElementWithChild = $compile('<li></li>')(scope);
|
||||
var menuElementChild = $compile('<a data-close-menu></a>')(scope);
|
||||
scope._expandedMenu = { menu: 'mobile' };
|
||||
|
||||
menuElementWithChild.appendTo(document.body);
|
||||
menuElementChild.appendTo(menuElementWithChild);
|
||||
menuElementChild.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
}));
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
describe('expandMenu Directive', function() {
|
||||
var menuElement, scope;
|
||||
|
||||
beforeEach(module('habitrpg'));
|
||||
|
||||
beforeEach(inject(function($rootScope, $compile) {
|
||||
scope = $rootScope.$new();
|
||||
|
||||
var element = '<a data-expand-menu menu="mobile"></a>';
|
||||
|
||||
menuElement = $compile(element)(scope);
|
||||
scope.$digest();
|
||||
}));
|
||||
|
||||
it('expands a connected menu when element is clicked', function() {
|
||||
expect(scope._expandedMenu).to.not.exist;
|
||||
menuElement.appendTo(document.body);
|
||||
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql('mobile')
|
||||
});
|
||||
|
||||
it('closes a connected menu when it is already open', function() {
|
||||
scope._expandedMenu = {};
|
||||
scope._expandedMenu.menu = 'mobile';
|
||||
menuElement.appendTo(document.body);
|
||||
|
||||
menuElement.triggerHandler('click');
|
||||
|
||||
expect(scope._expandedMenu.menu).to.eql(null)
|
||||
});
|
||||
});
|
||||
@@ -2,5 +2,10 @@
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": true,
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"habitrpg/browser",
|
||||
"habitrpg/mocha",
|
||||
"habitrpg/esnext",
|
||||
],
|
||||
}
|
||||
|
||||
19
test/client/unit/specs/components/inventory/drawer.js
Normal file
19
test/client/unit/specs/components/inventory/drawer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Vue from 'vue';
|
||||
import DrawerComponent from 'client/components/inventory/drawer.vue';
|
||||
|
||||
describe('DrawerComponent', () => {
|
||||
it('sets the correct default data', () => {
|
||||
expect(DrawerComponent.data).to.be.a('function');
|
||||
const defaultData = DrawerComponent.data();
|
||||
expect(defaultData.open).to.be.true;
|
||||
});
|
||||
|
||||
it('renders the correct title', () => {
|
||||
const Ctor = Vue.extend(DrawerComponent);
|
||||
const vm = new Ctor({propsData: {
|
||||
title: 'My title',
|
||||
}}).$mount();
|
||||
|
||||
expect(vm.$el.textContent).to.be.equal('My title');
|
||||
});
|
||||
});
|
||||
20
test/client/unit/specs/filters/roundBigNumber.js
Normal file
20
test/client/unit/specs/filters/roundBigNumber.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import roundBigNumberFilter from 'client/filters/roundBigNumber';
|
||||
|
||||
describe('round big number filter', () => {
|
||||
it('can round a decimal number', () => {
|
||||
expect(roundBigNumberFilter(4.567)).to.equal(4.57);
|
||||
expect(roundBigNumberFilter(4.562)).to.equal(4.56);
|
||||
});
|
||||
|
||||
it('can round thousands', () => {
|
||||
expect(roundBigNumberFilter(70065)).to.equal('70.1k');
|
||||
});
|
||||
|
||||
it('can round milions', () => {
|
||||
expect(roundBigNumberFilter(10000987)).to.equal('10.0m');
|
||||
});
|
||||
|
||||
it('can round bilions', () => {
|
||||
expect(roundBigNumberFilter(1000000000)).to.equal('1.0b');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import groupsUtilities from 'client/mixins/groupsUtilities';
|
||||
import { TAVERN_ID } from 'common/script/constants';
|
||||
import generateStore from 'client/store';
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('Groups Utilities Mixin', () => {
|
||||
@@ -7,6 +8,7 @@ describe('Groups Utilities Mixin', () => {
|
||||
|
||||
before(() => {
|
||||
instance = new Vue({
|
||||
store: generateStore(),
|
||||
mixins: [groupsUtilities],
|
||||
});
|
||||
|
||||
|
||||
@@ -121,6 +121,17 @@ describe('achievements', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('card achievements exist with counts', () => {
|
||||
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell'];
|
||||
cardTypes.forEach((card) => {
|
||||
let cardAchiev = basicAchievs[`${card}Cards`];
|
||||
|
||||
expect(cardAchiev).to.exist;
|
||||
expect(cardAchiev).to.have.property('optionalCount')
|
||||
.that.is.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('rebirth achievement exists with no count', () => {
|
||||
let rebirth = basicAchievs.rebirth;
|
||||
|
||||
@@ -174,7 +185,7 @@ describe('achievements', () => {
|
||||
});
|
||||
|
||||
it('card achievements exist with counts', () => {
|
||||
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
|
||||
let cardTypes = ['nye', 'valentine'];
|
||||
cardTypes.forEach((card) => {
|
||||
let cardAchiev = seasonalAchievs[`${card}Cards`];
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ describe('shops', () => {
|
||||
expect(shopCategories.length).to.be.greaterThan(2);
|
||||
});
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, (category) => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
let identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
|
||||
## Babel Paths for Production Environment
|
||||
|
||||
In development, we [transpile at server start](https://github.com/HabitRPG/habitrpg/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
|
||||
In development, we [transpile at server start](https://github.com/HabitRPG/habitica/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
|
||||
|
||||
This system means that requiring any files from `common/script` in `website/server/**/*.js` must be done through the `common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
|
||||
This system means that requiring any files from `website/common/script` in `website/server/**/*.js` must be done through the `website/common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
|
||||
|
||||
This test just verifies that none of the files in the server code are calling the common files directly.
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = {
|
||||
index: path.resolve(__dirname, '../../dist-client/index.html'),
|
||||
assetsRoot: path.resolve(__dirname, '../../dist-client'),
|
||||
assetsSubDirectory: 'static',
|
||||
assetsPublicPath: '/new-app',
|
||||
assetsPublicPath: '/new-app/',
|
||||
staticAssetsDirectory,
|
||||
productionSourceMap: true,
|
||||
// Gzip off by default as many popular static hosts such as
|
||||
|
||||
@@ -17,21 +17,31 @@ const baseConfig = {
|
||||
path: config.build.assetsRoot,
|
||||
publicPath: IS_PROD ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
|
||||
filename: '[name].js',
|
||||
devtoolModuleFilenameTemplate (info) {
|
||||
// Fix source maps, code from
|
||||
// https://github.com/Darkside73/bbsmile.com.ua/commit/3596d3c42ef91b69d8380359c3e8908edc08acdb
|
||||
let filename = info.resourcePath;
|
||||
if (info.resource.match(/\.vue$/) && !info.allLoaders.match(/type=script/)) {
|
||||
filename = 'generated';
|
||||
}
|
||||
|
||||
return filename;
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue', '.json'],
|
||||
modules: [
|
||||
path.join(__dirname, '..', 'website'),
|
||||
path.join(__dirname, '..', 'test/client/unit'),
|
||||
path.join(__dirname, '..', 'node_modules'),
|
||||
path.join(projectRoot, 'website'),
|
||||
path.join(projectRoot, 'test/client/unit'),
|
||||
path.join(projectRoot, 'node_modules'),
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery',
|
||||
website: path.resolve(__dirname, '../website'),
|
||||
common: path.resolve(__dirname, '../website/common'),
|
||||
client: path.resolve(__dirname, '../website/client'),
|
||||
assets: path.resolve(__dirname, '../website/client/assets'),
|
||||
components: path.resolve(__dirname, '../website/client/components'),
|
||||
website: path.resolve(projectRoot, 'website'),
|
||||
common: path.resolve(projectRoot, 'website/common'),
|
||||
client: path.resolve(projectRoot, 'website/client'),
|
||||
assets: path.resolve(projectRoot, 'website/client/assets'),
|
||||
components: path.resolve(projectRoot, 'website/client/components'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -63,11 +73,17 @@ const baseConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
include: projectRoot,
|
||||
exclude: /node_modules/,
|
||||
include: [
|
||||
path.join(projectRoot, 'test'),
|
||||
path.join(projectRoot, 'website'),
|
||||
path.join(projectRoot, 'node_modules', 'bootstrap-vue'),
|
||||
],
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
test: /\.(png|jpe?g|gif)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
@@ -82,6 +98,22 @@ const baseConfig = {
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{ loader: 'svg-inline-loader' },
|
||||
{ loader: 'svgo-loader' },
|
||||
],
|
||||
exclude: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{ loader: 'svg-url-loader' },
|
||||
{ loader: 'svgo-loader' },
|
||||
],
|
||||
include: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
BIN
website/assets/audio/arashiTheme/Achievement_Unlocked.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Achievement_Unlocked.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Achievement_Unlocked.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Achievement_Unlocked.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Chat.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Chat.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Chat.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Chat.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Daily.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Daily.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Daily.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Daily.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Death.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Death.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Death.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Death.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Item_Drop.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Item_Drop.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Item_Drop.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Item_Drop.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Level_Up.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Level_Up.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Level_Up.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Level_Up.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Minus_Habit.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Minus_Habit.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Minus_Habit.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Minus_Habit.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Plus_Habit.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Plus_Habit.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Plus_Habit.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Plus_Habit.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Reward.mp3
Normal file
BIN
website/assets/audio/arashiTheme/Reward.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/Reward.ogg
Normal file
BIN
website/assets/audio/arashiTheme/Reward.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/ToDo.mp3
Normal file
BIN
website/assets/audio/arashiTheme/ToDo.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/arashiTheme/ToDo.ogg
Normal file
BIN
website/assets/audio/arashiTheme/ToDo.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Achievement_Unlocked.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Achievement_Unlocked.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Achievement_Unlocked.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Achievement_Unlocked.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Chat.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Chat.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Chat.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Chat.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Daily.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Daily.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Daily.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Daily.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Death.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Death.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Death.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Death.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Item_Drop.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Item_Drop.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Item_Drop.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Item_Drop.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Minus_Habit.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Minus_Habit.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Minus_Habit.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Minus_Habit.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Plus_Habit.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Plus_Habit.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Plus_Habit.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Plus_Habit.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Reward.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Reward.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/Reward.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/Reward.ogg
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/ToDo.mp3
Normal file
BIN
website/assets/audio/beatscribeNesTheme/ToDo.mp3
Normal file
Binary file not shown.
BIN
website/assets/audio/beatscribeNesTheme/ToDo.ogg
Normal file
BIN
website/assets/audio/beatscribeNesTheme/ToDo.ogg
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,9 +1,9 @@
|
||||
/* Comment out for holiday events */
|
||||
.npc_ian {
|
||||
/* .npc_ian {
|
||||
background: url("/npc_ian.gif") no-repeat;
|
||||
width: 78px;
|
||||
height: 135px;
|
||||
}
|
||||
} */
|
||||
|
||||
.quest_burnout {
|
||||
background: url("/quest_burnout.gif") no-repeat;
|
||||
@@ -61,6 +61,10 @@
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.achievement-container {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user