Merged develop
12
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,21 +1,19 @@
|
|||||||
[//]: # (Before logging this issue, look through common problems at https://github.com/HabitRPG/habitrpg/issues If you find your issue there, read at least the first post to see if there is a workaround for you)
|
[//]: # (Before logging this issue, please post to the Report a Bug guild from the Habitica website's Help menu. Most bugs can be handled quickly there. If a GitHub issue is needed, you will be advised of that by a moderator or staff member -- a player with a dark blue or purple name. It is recommended that you don't create a new issue unless advised to.)
|
||||||
|
|
||||||
[//]: # (Github is primarily used for reporting bugs. If you have a feature request, use "Help > Request a Feature" so that the feature request can be vetted by the larger Habitica community)
|
[//]: # (Bugs in the mobile apps can also be reported there.)
|
||||||
|
|
||||||
[//]: # (To report a bug in one of the mobile apps, please report it in the correct repository. Android: https://github.com/HabitRPG/habitrpg-android, iOS: https://github.com/HabitRPG/habitrpg-ios)
|
[//]: # (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/habitrpg/issues/2760)
|
||||||
|
|
||||||
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
||||||
General Info
|
### General Info
|
||||||
* UUID:
|
* UUID:
|
||||||
* Browser:
|
* Browser:
|
||||||
* OS:
|
* OS:
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
[//]: # (Describe bug in detail here. Include pictures if helpful.)
|
[//]: # (Describe bug in detail here. Include screenshots if helpful.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Console Errors
|
#### Console Errors
|
||||||
[//]: # (Include any JavaScript console errors here.)
|
[//]: # (Include any JavaScript console errors here.)
|
||||||
|
|||||||
18
.travis.yml
@@ -12,22 +12,24 @@ before_install:
|
|||||||
- $CXX --version
|
- $CXX --version
|
||||||
- npm install -g npm@4
|
- 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
|
- 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:
|
before_script:
|
||||||
- npm run test:build
|
- npm run test:build
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
|
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
|
||||||
after_script:
|
script:
|
||||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
- npm run $TEST
|
||||||
script: npm run $TEST
|
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- CXX=g++-4.8
|
- CXX=g++-4.8
|
||||||
- DISABLE_REQUEST_LOGGING=true
|
- DISABLE_REQUEST_LOGGING=true
|
||||||
matrix:
|
matrix:
|
||||||
- TEST="lint"
|
- TEST="lint"
|
||||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
|
||||||
- TEST="test:sanity"
|
- TEST="test:sanity"
|
||||||
- TEST="test:content"
|
- TEST="test:content" COVERAGE=true
|
||||||
- TEST="test:common"
|
- TEST="test:common" COVERAGE=true
|
||||||
- TEST="test:karma"
|
- TEST="test:karma" COVERAGE=true
|
||||||
- TEST="client:unit"
|
- TEST="client:unit" COVERAGE=true
|
||||||
|
|||||||
37
Dockerfile
@@ -1,43 +1,16 @@
|
|||||||
FROM ubuntu:trusty
|
FROM node:boron
|
||||||
|
|
||||||
MAINTAINER Sabe Jones <sabe@habitica.com>
|
|
||||||
|
|
||||||
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
|
|
||||||
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
|
|
||||||
|
|
||||||
# Install prerequisites
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
libfontconfig1 \
|
|
||||||
libfreetype6 \
|
|
||||||
libkrb5-dev \
|
|
||||||
python
|
|
||||||
|
|
||||||
# Install NodeJS
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
|
||||||
RUN apt-get install -y nodejs
|
|
||||||
|
|
||||||
# Install npm@latest
|
|
||||||
RUN curl -sL https://www.npmjs.org/install.sh | sh
|
|
||||||
|
|
||||||
# Clean up package management
|
|
||||||
RUN apt-get clean
|
|
||||||
RUN rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install global packages
|
# Install global packages
|
||||||
RUN npm install -g gulp grunt-cli bower mocha
|
RUN npm install -g gulp grunt-cli bower mocha
|
||||||
|
|
||||||
# Clone Habitica repo and install dependencies
|
# Clone Habitica repo and install dependencies
|
||||||
WORKDIR /habitrpg
|
RUN mkdir -p /usr/src/habitrpg
|
||||||
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
|
WORKDIR /usr/src/habitrpg
|
||||||
|
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||||
RUN npm install
|
RUN npm install
|
||||||
RUN bower install --allow-root
|
RUN bower install --allow-root
|
||||||
|
|
||||||
# Create environment config file and build directory
|
# Create Build dir
|
||||||
RUN cp config.json.example config.json
|
|
||||||
RUN mkdir -p ./website/build
|
RUN mkdir -p ./website/build
|
||||||
|
|
||||||
# Start Habitica
|
# Start Habitica
|
||||||
|
|||||||
@@ -76,6 +76,11 @@
|
|||||||
"APN_ENABLED": "false",
|
"APN_ENABLED": "false",
|
||||||
"FCM_SERVER_API_KEY": ""
|
"FCM_SERVER_API_KEY": ""
|
||||||
},
|
},
|
||||||
|
"SITE_HTTP_AUTH": {
|
||||||
|
"ENABLED": "false",
|
||||||
|
"USERNAME": "admin",
|
||||||
|
"PASSWORD": "password"
|
||||||
|
},
|
||||||
"PUSHER": {
|
"PUSHER": {
|
||||||
"ENABLED": "false",
|
"ENABLED": "false",
|
||||||
"APP_ID": "appId",
|
"APP_ID": "appId",
|
||||||
@@ -87,5 +92,14 @@
|
|||||||
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||||
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
|
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
|
||||||
},
|
},
|
||||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111"
|
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||||
|
"EMAILS" : {
|
||||||
|
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
|
||||||
|
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
|
||||||
|
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
|
||||||
|
},
|
||||||
|
"LOGGLY" : {
|
||||||
|
"TOKEN" : "example-token",
|
||||||
|
"SUBDOMAIN" : "exmaple-subdomain"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
|
|||||||
|
|
||||||
gulp.task('test:api-v3:unit', (done) => {
|
gulp.task('test:api-v3:unit', (done) => {
|
||||||
let runner = exec(
|
let runner = exec(
|
||||||
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||||
(err, stdout, stderr) => {
|
(err, stdout, stderr) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -298,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
|
|||||||
|
|
||||||
gulp.task('test:api-v3:integration', (done) => {
|
gulp.task('test:api-v3:integration', (done) => {
|
||||||
let runner = exec(
|
let runner = exec(
|
||||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||||
{maxBuffer: 500 * 1024},
|
{maxBuffer: 500 * 1024},
|
||||||
(err, stdout, stderr) => {
|
(err, stdout, stderr) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
88
migrations/20170418_subscriber_jackalopes.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
var migrationName = '20170418_subscriber_jackalopes.js';
|
||||||
|
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||||
|
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Award Royal Purple Jackalope pet to all current subscribers
|
||||||
|
*/
|
||||||
|
|
||||||
|
var monk = require('monk');
|
||||||
|
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||||
|
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||||
|
var now = new Date();
|
||||||
|
|
||||||
|
function processUsers(lastId) {
|
||||||
|
// specify a query to limit the affected users (empty for all users):
|
||||||
|
var query = {
|
||||||
|
'purchased.plan.customerId': {$type: 2},
|
||||||
|
$or: [
|
||||||
|
{'purchased.plan.dateTerminated': null},
|
||||||
|
{'purchased.plan.dateTerminated': {$exists: false}},
|
||||||
|
{'purchased.plan.dateTerminated': {$gt: now}},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
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):
|
||||||
|
})
|
||||||
|
.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 = {'items.pets.Jackalope-RoyalPurple': 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;
|
||||||
207
migrations/20170425_missing_incentives.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
var migrationName = '20170425_missing_incentives';
|
||||||
|
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||||
|
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Award missing Royal Purple Hatching Potion to users with 55+ check-ins
|
||||||
|
* Reduce users with impossible check-in counts to a reasonable number
|
||||||
|
*/
|
||||||
|
|
||||||
|
import monk from 'monk';
|
||||||
|
import common from '../website/common';
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
'loginIncentives': {$gt:99},
|
||||||
|
'migration': {$ne: migrationName},
|
||||||
|
};
|
||||||
|
|
||||||
|
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):
|
||||||
|
})
|
||||||
|
.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 language = user.preferences.language || 'en';
|
||||||
|
var set = {'migration': migrationName};
|
||||||
|
var inc = {
|
||||||
|
'items.eggs.BearCub': 0,
|
||||||
|
'items.eggs.Cactus': 0,
|
||||||
|
'items.eggs.Dragon': 0,
|
||||||
|
'items.eggs.FlyingPig': 0,
|
||||||
|
'items.eggs.Fox': 0,
|
||||||
|
'items.eggs.LionCub': 0,
|
||||||
|
'items.eggs.PandaCub': 0,
|
||||||
|
'items.eggs.TigerCub': 0,
|
||||||
|
'items.eggs.Wolf': 0,
|
||||||
|
'items.food.Chocolate': 0,
|
||||||
|
'items.food.CottonCandyBlue': 0,
|
||||||
|
'items.food.CottonCandyPink': 0,
|
||||||
|
'items.food.Fish': 0,
|
||||||
|
'items.food.Honey': 0,
|
||||||
|
'items.food.Meat': 0,
|
||||||
|
'items.food.Milk': 0,
|
||||||
|
'items.food.Potatoe': 0,
|
||||||
|
'items.food.RottenMeat': 0,
|
||||||
|
'items.food.Strawberry': 0,
|
||||||
|
'items.hatchingPotions.Base': 0,
|
||||||
|
'items.hatchingPotions.CottonCandyBlue': 0,
|
||||||
|
'items.hatchingPotions.CottonCandyPink': 0,
|
||||||
|
'items.hatchingPotions.Desert': 0,
|
||||||
|
'items.hatchingPotions.Golden': 0,
|
||||||
|
'items.hatchingPotions.Red': 0,
|
||||||
|
'items.hatchingPotions.RoyalPurple': 0,
|
||||||
|
'items.hatchingPotions.Shade': 0,
|
||||||
|
'items.hatchingPotions.Skeleton': 0,
|
||||||
|
'items.hatchingPotions.White': 0,
|
||||||
|
'items.hatchingPotions.Zombie': 0,
|
||||||
|
};
|
||||||
|
var nextReward;
|
||||||
|
|
||||||
|
if (user.loginIncentives >= 105) {
|
||||||
|
inc['items.hatchingPotions.RoyalPurple'] += 1;
|
||||||
|
nextReward = 110;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 110) {
|
||||||
|
inc['items.eggs.BearCub'] += 1;
|
||||||
|
inc['items.eggs.Cactus'] += 1;
|
||||||
|
inc['items.eggs.Dragon'] += 1;
|
||||||
|
inc['items.eggs.FlyingPig'] += 1;
|
||||||
|
inc['items.eggs.Fox'] += 1;
|
||||||
|
inc['items.eggs.LionCub'] += 1;
|
||||||
|
inc['items.eggs.PandaCub'] += 1;
|
||||||
|
inc['items.eggs.TigerCub'] += 1;
|
||||||
|
inc['items.eggs.Wolf'] += 1;
|
||||||
|
nextReward = 115;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 115) {
|
||||||
|
inc['items.hatchingPotions.RoyalPurple'] += 1;
|
||||||
|
nextReward = 120;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 120) {
|
||||||
|
inc['items.hatchingPotions.Base'] += 1;
|
||||||
|
inc['items.hatchingPotions.CottonCandyBlue'] += 1;
|
||||||
|
inc['items.hatchingPotions.CottonCandyPink'] += 1;
|
||||||
|
inc['items.hatchingPotions.Desert'] += 1;
|
||||||
|
inc['items.hatchingPotions.Golden'] += 1;
|
||||||
|
inc['items.hatchingPotions.Red'] += 1;
|
||||||
|
inc['items.hatchingPotions.Shade'] += 1;
|
||||||
|
inc['items.hatchingPotions.Skeleton'] += 1;
|
||||||
|
inc['items.hatchingPotions.White'] += 1;
|
||||||
|
inc['items.hatchingPotions.Zombie'] += 1;
|
||||||
|
nextReward = 125;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 125) {
|
||||||
|
inc['items.hatchingPotions.RoyalPurple'] += 1;
|
||||||
|
nextReward = 130;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 130) {
|
||||||
|
inc['items.food.Chocolate'] += 3;
|
||||||
|
inc['items.food.CottonCandyBlue'] += 3;
|
||||||
|
inc['items.food.CottonCandyPink'] += 3;
|
||||||
|
inc['items.food.Fish'] += 3;
|
||||||
|
inc['items.food.Honey'] += 3;
|
||||||
|
inc['items.food.Meat'] += 3;
|
||||||
|
inc['items.food.Milk'] += 3;
|
||||||
|
inc['items.food.Potatoe'] += 3;
|
||||||
|
inc['items.food.RottenMeat'] += 3;
|
||||||
|
inc['items.food.Strawberry'] += 3;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 135) {
|
||||||
|
inc['items.hatchingPotions.RoyalPurple'] += 1;
|
||||||
|
nextReward = 140;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 140) {
|
||||||
|
set['items.gear.owned.weapon_special_skeletonKey'] = true;
|
||||||
|
set['items.gear.owned.shield_special_lootBag'] = true;
|
||||||
|
nextReward = 145;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 145) {
|
||||||
|
inc['items.hatchingPotions.RoyalPurple'] += 1;
|
||||||
|
nextReward = 150;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives >= 150) {
|
||||||
|
set['items.gear.owned.head_special_clandestineCowl'] = true;
|
||||||
|
set['items.gear.owned.armor_special_sneakthiefRobes'] = true;
|
||||||
|
nextReward = 155;
|
||||||
|
}
|
||||||
|
if (user.loginIncentives > 155) {
|
||||||
|
set.loginIncentives = 155;
|
||||||
|
nextReward = 160;
|
||||||
|
}
|
||||||
|
|
||||||
|
var push = {
|
||||||
|
'notifications': {
|
||||||
|
'type': 'LOGIN_INCENTIVE',
|
||||||
|
'data': {
|
||||||
|
'nextRewardAt': nextReward,
|
||||||
|
'rewardKey': [
|
||||||
|
'shop_armoire',
|
||||||
|
],
|
||||||
|
'rewardText': common.i18n.t('checkInRewards', language),
|
||||||
|
'reward': [],
|
||||||
|
'message': common.i18n.t('backloggedCheckInRewards', language),
|
||||||
|
},
|
||||||
|
'id': common.uuid(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dbUsers.update({_id: user._id}, {$set:set, $push:push, $inc:inc});
|
||||||
|
|
||||||
|
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||||
|
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayData() {
|
||||||
|
console.warn('\n' + count + ' users processed\n');
|
||||||
|
return exiting(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exiting(code, msg) {
|
||||||
|
code = code || 0; // 0 = success
|
||||||
|
if (code && !msg) { msg = 'ERROR!'; }
|
||||||
|
if (msg) {
|
||||||
|
if (code) { console.error(msg); }
|
||||||
|
else { console.log( msg); }
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = processUsers;
|
||||||
47
migrations/challenges/sync-all-challenges.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import Bluebird from 'Bluebird';
|
||||||
|
|
||||||
|
import { model as Challenges } from '../../website/server/models/challenge';
|
||||||
|
import { model as User } from '../../website/server/models/user';
|
||||||
|
|
||||||
|
async function syncChallengeToMembers (challenges) {
|
||||||
|
let challengSyncPromises = challenges.map(async function (challenge) {
|
||||||
|
let users = await User.find({
|
||||||
|
// _id: '',
|
||||||
|
challenges: challenge._id,
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
let promises = [];
|
||||||
|
users.forEach(function (user) {
|
||||||
|
promises.push(challenge.syncToUser(user));
|
||||||
|
promises.push(challenge.save());
|
||||||
|
promises.push(user.save());
|
||||||
|
});
|
||||||
|
|
||||||
|
return Bluebird.all(promises);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Bluebird.all(challengSyncPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncChallenges (lastChallengeDate) {
|
||||||
|
let query = {
|
||||||
|
// _id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastChallengeDate) {
|
||||||
|
query.createdOn = { $lte: lastChallengeDate };
|
||||||
|
}
|
||||||
|
|
||||||
|
let challengesFound = await Challenges.find(query)
|
||||||
|
.limit(10)
|
||||||
|
.sort('-createdAt')
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
let syncedChallenges = await syncChallengeToMembers(challengesFound)
|
||||||
|
.catch(reason => console.error(reason));
|
||||||
|
let lastChallenge = challengesFound[challengesFound.length - 1];
|
||||||
|
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
|
||||||
|
return syncedChallenges;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = syncChallenges;
|
||||||
@@ -5,11 +5,12 @@ var authorUuid = ''; //... own data is done
|
|||||||
/*
|
/*
|
||||||
* This migrations will add a free subscription to a specified group
|
* This migrations will add a free subscription to a specified group
|
||||||
*/
|
*/
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import { model as Group } from '../../website/server/models/group';
|
import { model as Group } from '../../website/server/models/group';
|
||||||
|
|
||||||
// @TODO: this should probably be a GroupManager library method
|
// @TODO: this should probably be a GroupManager library method
|
||||||
async function addUnlimitedSubscription (groupId) {
|
async function addUnlimitedSubscription (groupId, dateTerminated) {
|
||||||
let group = await Group.findById(groupId);
|
let group = await Group.findById(groupId);
|
||||||
|
|
||||||
group.purchased.plan.customerId = "group-unlimited";
|
group.purchased.plan.customerId = "group-unlimited";
|
||||||
@@ -18,6 +19,10 @@ async function addUnlimitedSubscription (groupId) {
|
|||||||
group.purchased.plan.paymentMethod = "Group Unlimited";
|
group.purchased.plan.paymentMethod = "Group Unlimited";
|
||||||
group.purchased.plan.planId = "group_monthly";
|
group.purchased.plan.planId = "group_monthly";
|
||||||
group.purchased.plan.dateTerminated = null;
|
group.purchased.plan.dateTerminated = null;
|
||||||
|
if (dateTerminated) {
|
||||||
|
let dateToEnd = moment(dateTerminated).toDate();
|
||||||
|
group.purchased.plan.dateTerminated = dateToEnd;
|
||||||
|
}
|
||||||
// group.purchased.plan.owner = ObjectId();
|
// group.purchased.plan.owner = ObjectId();
|
||||||
group.purchased.plan.subscriptionId = "";
|
group.purchased.plan.subscriptionId = "";
|
||||||
|
|
||||||
@@ -29,5 +34,7 @@ module.exports = async function addUnlimitedSubscriptionCreator () {
|
|||||||
|
|
||||||
if (!groupId) throw Error('Group ID is required');
|
if (!groupId) throw Error('Group ID is required');
|
||||||
|
|
||||||
let result = await addUnlimitedSubscription(groupId)
|
let dateTerminated = process.argv[3];
|
||||||
|
|
||||||
|
let result = await addUnlimitedSubscription(groupId, dateTerminated);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ var _id = '';
|
|||||||
var update = {
|
var update = {
|
||||||
$addToSet: {
|
$addToSet: {
|
||||||
'purchased.plan.mysteryItems':{
|
'purchased.plan.mysteryItems':{
|
||||||
$each:['head_mystery_201702','back_mystery_201702']
|
$each:['back_mystery_201704','armor_mystery_201704']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
var migrationName = '20170201_takeThis.js'; // Update per month
|
var migrationName = '20170502_takeThis.js'; // Update per month
|
||||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ function processUsers(lastId) {
|
|||||||
// specify a query to limit the affected users (empty for all users):
|
// specify a query to limit the affected users (empty for all users):
|
||||||
var query = {
|
var query = {
|
||||||
'migration':{$ne:migrationName},
|
'migration':{$ne:migrationName},
|
||||||
'challenges':{$in:['b1d436b5-c784-42e3-9b07-7072479a6f8e']} // Update per month
|
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastId) {
|
if (lastId) {
|
||||||
|
|||||||
2233
npm-shrinkwrap.json
generated
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||||
"version": "3.80.0",
|
"version": "3.89.0",
|
||||||
"main": "./website/server/index.js",
|
"main": "./website/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@slack/client": "^3.8.1",
|
"@slack/client": "^3.8.1",
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"domain-middleware": "~0.1.0",
|
"domain-middleware": "~0.1.0",
|
||||||
"estraverse": "^4.1.1",
|
"estraverse": "^4.1.1",
|
||||||
"express": "~4.14.0",
|
"express": "~4.14.0",
|
||||||
|
"express-basic-auth": "^1.0.1",
|
||||||
"express-csv": "~0.6.0",
|
"express-csv": "~0.6.0",
|
||||||
"express-validator": "^2.18.0",
|
"express-validator": "^2.18.0",
|
||||||
"extract-text-webpack-plugin": "^2.0.0-rc.3",
|
"extract-text-webpack-plugin": "^2.0.0-rc.3",
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
"grunt-contrib-stylus": "~0.20.0",
|
"grunt-contrib-stylus": "~0.20.0",
|
||||||
"grunt-contrib-uglify": "~0.6.0",
|
"grunt-contrib-uglify": "~0.6.0",
|
||||||
"grunt-contrib-watch": "~0.6.1",
|
"grunt-contrib-watch": "~0.6.1",
|
||||||
"grunt-hashres": "~0.4.1",
|
"grunt-hashres": "habitrpg/grunt-hashres#v0.4.2",
|
||||||
"gulp": "^3.9.0",
|
"gulp": "^3.9.0",
|
||||||
"gulp-babel": "^6.1.2",
|
"gulp-babel": "^6.1.2",
|
||||||
"gulp-grunt": "^0.5.2",
|
"gulp-grunt": "^0.5.2",
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
"webpack": "^2.2.1",
|
"webpack": "^2.2.1",
|
||||||
"webpack-merge": "^2.6.1",
|
"webpack-merge": "^2.6.1",
|
||||||
"winston": "^2.1.0",
|
"winston": "^2.1.0",
|
||||||
|
"winston-loggly-bulk": "^1.4.2",
|
||||||
"xml2js": "^0.4.4"
|
"xml2js": "^0.4.4"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -138,9 +140,9 @@
|
|||||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||||
"test:api-v3:integration": "gulp test:api-v3:integration",
|
"test:api-v3:integration": "gulp test:api-v3:integration",
|
||||||
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
|
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
|
||||||
"test:sanity": "mocha test/sanity --recursive",
|
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
|
||||||
"test:common": "mocha test/common --recursive",
|
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
|
||||||
"test:content": "mocha test/content --recursive",
|
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
|
||||||
"test:karma": "karma start test/client-old/spec/karma.conf.js --single-run",
|
"test:karma": "karma start test/client-old/spec/karma.conf.js --single-run",
|
||||||
"test:karma:watch": "karma start test/client-old/spec/karma.conf.js",
|
"test:karma:watch": "karma start test/client-old/spec/karma.conf.js",
|
||||||
"test:prepare:webdriver": "webdriver-manager update",
|
"test:prepare:webdriver": "webdriver-manager update",
|
||||||
@@ -182,7 +184,7 @@
|
|||||||
"grunt-karma": "~0.12.1",
|
"grunt-karma": "~0.12.1",
|
||||||
"http-proxy-middleware": "^0.17.0",
|
"http-proxy-middleware": "^0.17.0",
|
||||||
"inject-loader": "^3.0.0-beta4",
|
"inject-loader": "^3.0.0-beta4",
|
||||||
"istanbul": "^0.3.14",
|
"istanbul": "^1.1.0-alpha.1",
|
||||||
"karma": "^1.3.0",
|
"karma": "^1.3.0",
|
||||||
"karma-babel-preprocessor": "^6.0.1",
|
"karma-babel-preprocessor": "^6.0.1",
|
||||||
"karma-chai-plugins": "~0.6.0",
|
"karma-chai-plugins": "~0.6.0",
|
||||||
@@ -197,7 +199,7 @@
|
|||||||
"karma-webpack": "^2.0.2",
|
"karma-webpack": "^2.0.2",
|
||||||
"lcov-result-merger": "^1.0.2",
|
"lcov-result-merger": "^1.0.2",
|
||||||
"lolex": "^1.4.0",
|
"lolex": "^1.4.0",
|
||||||
"mocha": "^2.3.3",
|
"mocha": "^3.2.0",
|
||||||
"mongodb": "^2.0.46",
|
"mongodb": "^2.0.46",
|
||||||
"mongoskin": "~2.1.0",
|
"mongoskin": "~2.1.0",
|
||||||
"monk": "^4.0.0",
|
"monk": "^4.0.0",
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import {
|
|||||||
sleep,
|
sleep,
|
||||||
server,
|
server,
|
||||||
} from '../../../../helpers/api-v3-integration.helper';
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
import {
|
||||||
|
SPAM_MESSAGE_LIMIT,
|
||||||
|
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||||
|
TAVERN_ID,
|
||||||
|
} from '../../../../../website/server/models/group';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
describe('POST /chat', () => {
|
describe('POST /chat', () => {
|
||||||
let user, groupWithChat, userWithChatRevoked, member;
|
let user, groupWithChat, member, additionalMember;
|
||||||
let testMessage = 'Test Message';
|
let testMessage = 'Test Message';
|
||||||
|
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
@@ -22,8 +28,8 @@ describe('POST /chat', () => {
|
|||||||
|
|
||||||
user = groupLeader;
|
user = groupLeader;
|
||||||
groupWithChat = group;
|
groupWithChat = group;
|
||||||
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
|
|
||||||
member = members[0];
|
member = members[0];
|
||||||
|
additionalMember = members[1];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns an error when no message is provided', async () => {
|
it('Returns an error when no message is provided', async () => {
|
||||||
@@ -62,6 +68,7 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
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({
|
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||||
code: 404,
|
code: 404,
|
||||||
error: 'NotFound',
|
error: 'NotFound',
|
||||||
@@ -69,6 +76,96 @@ describe('POST /chat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('banned word', () => {
|
||||||
|
it('returns an error when chat message contains a banned word in tavern', async () => {
|
||||||
|
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('bannedWordUsed'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when word is part of a phrase', async () => {
|
||||||
|
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
|
||||||
|
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('bannedWordUsed'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when word is surrounded by non alphabet characters', async () => {
|
||||||
|
let wordInPhrase = `_!${testBannedWordMessage}@_`;
|
||||||
|
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('bannedWordUsed'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when bad word is suffix of a word', async () => {
|
||||||
|
let wordAsSuffix = `prefix${testBannedWordMessage}`;
|
||||||
|
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when bad word is prefix of a word', async () => {
|
||||||
|
let wordAsPrefix = `${testBannedWordMessage}suffix`;
|
||||||
|
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when sending a chat message containing a banned word to a party', async () => {
|
||||||
|
let { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'Party',
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
|
||||||
|
let { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'public guild',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
|
||||||
|
let { group, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: 'private guild',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
|
||||||
|
|
||||||
|
expect(message.message.id).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
|
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
|
||||||
let { group, members } = await createAndPopulateGroup({
|
let { group, members } = await createAndPopulateGroup({
|
||||||
groupDetails: {
|
groupDetails: {
|
||||||
@@ -173,4 +270,30 @@ describe('POST /chat', () => {
|
|||||||
expect(message.message.id).to.exist;
|
expect(message.message.id).to.exist;
|
||||||
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
|
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Spam prevention', () => {
|
||||||
|
it('Returns an error when the user has been posting too many messages', async () => {
|
||||||
|
// Post as many messages are needed to reach the spam limit
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||||
|
expect(result.message.id).to.exist;
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('messageGroupChatSpam'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contributor should not receive spam alert', async () => {
|
||||||
|
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
|
||||||
|
|
||||||
|
// Post 1 more message than the spam limit to ensure they do not reach the limit
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
|
||||||
|
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||||
|
expect(result.message.id).to.exist;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,15 +63,17 @@ describe('GET /groups/:groupId/invites', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns only first 30 invites', async () => {
|
it('returns only first 30 invites', async () => {
|
||||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
let leader = await generateUser({balance: 4});
|
||||||
|
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||||
|
|
||||||
let invitesToGenerate = [];
|
let invitesToGenerate = [];
|
||||||
for (let i = 0; i < 31; i++) {
|
for (let i = 0; i < 31; i++) {
|
||||||
invitesToGenerate.push(generateUser());
|
invitesToGenerate.push(generateUser());
|
||||||
}
|
}
|
||||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||||
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
await leader.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||||
|
|
||||||
let res = await user.get('/groups/party/invites');
|
let res = await leader.get(`/groups/${group._id}/invites`);
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
createAndPopulateGroup,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
describe('POST /group/:groupId/remove-manager', () => {
|
||||||
|
let leader, nonLeader, groupToUpdate;
|
||||||
|
let groupName = 'Test Public Guild';
|
||||||
|
let groupType = 'guild';
|
||||||
|
let nonManager;
|
||||||
|
|
||||||
|
function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.group.id === groupToUpdate._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: groupName,
|
||||||
|
type: groupType,
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupToUpdate = group;
|
||||||
|
leader = groupLeader;
|
||||||
|
nonLeader = members[0];
|
||||||
|
nonManager = members[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when a non group leader tries to add member', async () => {
|
||||||
|
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when manager does not exist', async () => {
|
||||||
|
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||||
|
managerId: nonManager._id,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('userIsNotManager'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows a leader to remove managers', async () => {
|
||||||
|
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes group approval notifications from a manager that is removed', async () => {
|
||||||
|
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
});
|
||||||
|
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
|
||||||
|
text: 'test todo',
|
||||||
|
type: 'todo',
|
||||||
|
requiresApproval: true,
|
||||||
|
});
|
||||||
|
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
|
||||||
|
let memberTasks = await leader.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('taskApprovalHasBeenRequested'),
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await nonLeader.sync();
|
||||||
|
|
||||||
|
expect(nonLeader.notifications.length).to.equal(0);
|
||||||
|
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,8 +4,11 @@ import {
|
|||||||
translate as t,
|
translate as t,
|
||||||
} from '../../../../helpers/api-v3-integration.helper';
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
const INVITES_LIMIT = 100;
|
const INVITES_LIMIT = 100;
|
||||||
|
const PARTY_LIMIT_MEMBERS = 30;
|
||||||
|
const MAX_EMAIL_INVITES_BY_USER = 200;
|
||||||
|
|
||||||
describe('Post /groups/:groupId/invite', () => {
|
describe('Post /groups/:groupId/invite', () => {
|
||||||
let inviter;
|
let inviter;
|
||||||
@@ -204,13 +207,37 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns an error when a user has sent the max number of email invites', async () => {
|
||||||
|
let inviterWithMax = await generateUser({
|
||||||
|
invitesSent: MAX_EMAIL_INVITES_BY_USER,
|
||||||
|
balance: 4,
|
||||||
|
});
|
||||||
|
let tmpGroup = await inviterWithMax.post('/groups', {
|
||||||
|
name: groupName,
|
||||||
|
type: 'guild',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
|
||||||
|
emails: [testInvite],
|
||||||
|
inviter: 'inviter name',
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('invites a user to a group by email', async () => {
|
it('invites a user to a group by email', async () => {
|
||||||
let res = await inviter.post(`/groups/${group._id}/invite`, {
|
let res = await inviter.post(`/groups/${group._id}/invite`, {
|
||||||
emails: [testInvite],
|
emails: [testInvite],
|
||||||
inviter: 'inviter name',
|
inviter: 'inviter name',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updatedUser = await inviter.sync();
|
||||||
|
|
||||||
expect(res).to.exist;
|
expect(res).to.exist;
|
||||||
|
expect(updatedUser.invitesSent).to.eql(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invites multiple users to a group by email', async () => {
|
it('invites multiple users to a group by email', async () => {
|
||||||
@@ -218,7 +245,10 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}],
|
emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updatedUser = await inviter.sync();
|
||||||
|
|
||||||
expect(res).to.exist;
|
expect(res).to.exist;
|
||||||
|
expect(updatedUser.invitesSent).to.eql(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,6 +351,19 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows 30+ members in a guild', async () => {
|
||||||
|
let invitesToGenerate = [];
|
||||||
|
// Generate 30 users to invite (30 + leader = 31 members)
|
||||||
|
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
|
||||||
|
invitesToGenerate.push(generateUser());
|
||||||
|
}
|
||||||
|
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||||
|
// Invite users
|
||||||
|
expect(await inviter.post(`/groups/${group._id}/invite`, {
|
||||||
|
uuids: generatedInvites.map(invite => invite._id),
|
||||||
|
})).to.be.an('array');
|
||||||
|
});
|
||||||
|
|
||||||
// @TODO: Add this after we are able to mock the group plan route
|
// @TODO: Add this after we are able to mock the group plan route
|
||||||
xit('returns an error when a non-leader invites to a group plan', async () => {
|
xit('returns an error when a non-leader invites to a group plan', async () => {
|
||||||
let userToInvite = await generateUser();
|
let userToInvite = await generateUser();
|
||||||
@@ -410,5 +453,36 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
});
|
});
|
||||||
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
|
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows 30 members in a party', async () => {
|
||||||
|
let invitesToGenerate = [];
|
||||||
|
// Generate 29 users to invite (29 + leader = 30 members)
|
||||||
|
for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i++) {
|
||||||
|
invitesToGenerate.push(generateUser());
|
||||||
|
}
|
||||||
|
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||||
|
// Invite users
|
||||||
|
expect(await inviter.post(`/groups/${party._id}/invite`, {
|
||||||
|
uuids: generatedInvites.map(invite => invite._id),
|
||||||
|
})).to.be.an('array');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow 30+ members in a party', async () => {
|
||||||
|
let invitesToGenerate = [];
|
||||||
|
// Generate 30 users to invite (30 + leader = 31 members)
|
||||||
|
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
|
||||||
|
invitesToGenerate.push(generateUser());
|
||||||
|
}
|
||||||
|
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||||
|
// Invite users
|
||||||
|
await expect(inviter.post(`/groups/${party._id}/invite`, {
|
||||||
|
uuids: generatedInvites.map(invite => invite._id),
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('partyExceedsMembersLimit', {maxMembersParty: PARTY_LIMIT_MEMBERS}),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
85
test/api/v3/integration/groups/POST-groups_manager.test.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
createAndPopulateGroup,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
|
||||||
|
describe('POST /group/:groupId/add-manager', () => {
|
||||||
|
let leader, nonLeader, groupToUpdate;
|
||||||
|
let groupName = 'Test Public Guild';
|
||||||
|
let groupType = 'guild';
|
||||||
|
let nonMember;
|
||||||
|
|
||||||
|
context('Guilds', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: groupName,
|
||||||
|
type: groupType,
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupToUpdate = group;
|
||||||
|
leader = groupLeader;
|
||||||
|
nonLeader = members[0];
|
||||||
|
nonMember = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when a non group leader tries to add member', async () => {
|
||||||
|
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when trying to promote a non member', async () => {
|
||||||
|
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||||
|
managerId: nonMember._id,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('userMustBeMember'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows a leader to add managers', async () => {
|
||||||
|
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||||
|
managerId: nonLeader._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Party', () => {
|
||||||
|
let party, partyLeader, partyNonLeader;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: groupName,
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
party = group;
|
||||||
|
partyLeader = groupLeader;
|
||||||
|
partyNonLeader = members[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows leader of party to add managers', async () => {
|
||||||
|
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
|
||||||
|
managerId: partyNonLeader._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from '../../../../../helpers/api-integration/v3';
|
} from '../../../../../helpers/api-integration/v3';
|
||||||
import { find } from 'lodash';
|
import { find } from 'lodash';
|
||||||
|
|
||||||
describe('DELETE /tasks/:id', () => {
|
describe('Groups DELETE /tasks/:id', () => {
|
||||||
let user, guild, member, member2, task;
|
let user, guild, member, member2, task;
|
||||||
|
|
||||||
function findAssignedTask (memberTask) {
|
function findAssignedTask (memberTask) {
|
||||||
@@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a manager to delete a group task', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.del(`/tasks/${task._id}`);
|
||||||
|
|
||||||
|
await expect(user.get(`/tasks/${task._id}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('unlinks assigned user', async () => {
|
it('unlinks assigned user', async () => {
|
||||||
await user.del(`/tasks/${task._id}`);
|
await user.del(`/tasks/${task._id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
|
|||||||
let approvals = await user.get(`/approvals/group/${guild._id}`);
|
let approvals = await user.get(`/approvals/group/${guild._id}`);
|
||||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows managers to get a list of task that need approval', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let approvals = await member.get(`/approvals/group/${guild._id}`);
|
||||||
|
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import { find } from 'lodash';
|
import { find } from 'lodash';
|
||||||
|
|
||||||
describe('POST /tasks/:id/approve/:userId', () => {
|
describe('POST /tasks/:id/approve/:userId', () => {
|
||||||
let user, guild, member, task;
|
let user, guild, member, member2, task;
|
||||||
|
|
||||||
function findAssignedTask (memberTask) {
|
function findAssignedTask (memberTask) {
|
||||||
return memberTask.group.id === guild._id;
|
return memberTask.group.id === guild._id;
|
||||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
|||||||
name: 'Test Guild',
|
name: 'Test Guild',
|
||||||
type: 'guild',
|
type: 'guild',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
guild = group;
|
guild = group;
|
||||||
user = groupLeader;
|
user = groupLeader;
|
||||||
member = members[0];
|
member = members[0];
|
||||||
|
member2 = members[1];
|
||||||
|
|
||||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||||
text: 'test todo',
|
text: 'test todo',
|
||||||
@@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
|||||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a manager to approve an assigned user', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||||
|
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
await member.sync();
|
||||||
|
|
||||||
|
expect(member.notifications.length).to.equal(2);
|
||||||
|
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
|
||||||
|
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||||
|
expect(member.notifications[1].type).to.equal('SCORED_TASK');
|
||||||
|
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||||
|
|
||||||
|
expect(syncedTask.group.approval.approved).to.be.true;
|
||||||
|
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
|
||||||
|
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes approval pending notifications from managers', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('taskApprovalHasBeenRequested'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
await member2.sync();
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||||
|
expect(member2.notifications.length).to.equal(1);
|
||||||
|
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
await member2.sync();
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(0);
|
||||||
|
expect(member2.notifications.length).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents double approval on a task', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||||
|
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('canOnlyApproveTaskOnce'),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import { find } from 'lodash';
|
import { find } from 'lodash';
|
||||||
|
|
||||||
describe('POST /tasks/:id/score/:direction', () => {
|
describe('POST /tasks/:id/score/:direction', () => {
|
||||||
let user, guild, member, task;
|
let user, guild, member, member2, task;
|
||||||
|
|
||||||
function findAssignedTask (memberTask) {
|
function findAssignedTask (memberTask) {
|
||||||
return memberTask.group.id === guild._id;
|
return memberTask.group.id === guild._id;
|
||||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
name: 'Test Guild',
|
name: 'Test Guild',
|
||||||
type: 'guild',
|
type: 'guild',
|
||||||
},
|
},
|
||||||
members: 1,
|
members: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
guild = group;
|
guild = group;
|
||||||
user = groupLeader;
|
user = groupLeader;
|
||||||
member = members[0];
|
member = members[0];
|
||||||
|
member2 = members[1];
|
||||||
|
|
||||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||||
text: 'test todo',
|
text: 'test todo',
|
||||||
@@ -56,6 +57,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||||
user: member.auth.local.username,
|
user: member.auth.local.username,
|
||||||
taskName: updatedTask.text,
|
taskName: updatedTask.text,
|
||||||
|
taskId: updatedTask._id,
|
||||||
}, 'cs')); // This test only works if we have the notification translated
|
}, 'cs')); // This test only works if we have the notification translated
|
||||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||||
|
|
||||||
@@ -63,6 +65,42 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends notifications to all managers', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('taskApprovalHasBeenRequested'),
|
||||||
|
});
|
||||||
|
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||||
|
await user.sync();
|
||||||
|
await member2.sync();
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||||
|
expect(user.notifications[0].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(member2.notifications.length).to.equal(1);
|
||||||
|
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||||
|
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||||
|
user: member.auth.local.username,
|
||||||
|
taskName: updatedTask.text,
|
||||||
|
taskId: updatedTask._id,
|
||||||
|
}));
|
||||||
|
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||||
|
});
|
||||||
|
|
||||||
it('errors when approval has already been requested', async () => {
|
it('errors when approval has already been requested', async () => {
|
||||||
let memberTasks = await member.get('/tasks/user');
|
let memberTasks = await member.get('/tasks/user');
|
||||||
let syncedTask = find(memberTasks, findAssignedTask);
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
generateGroup,
|
createAndPopulateGroup,
|
||||||
translate as t,
|
translate as t,
|
||||||
} from '../../../../../helpers/api-v3-integration.helper';
|
} from '../../../../../helpers/api-v3-integration.helper';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
describe('POST /tasks/group/:groupid', () => {
|
describe('POST /tasks/group/:groupid', () => {
|
||||||
let user, guild;
|
let user, guild, manager;
|
||||||
|
let groupName = 'Test Public Guild';
|
||||||
|
let groupType = 'guild';
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await generateUser({balance: 1});
|
user = await generateUser({balance: 1});
|
||||||
guild = await generateGroup(user, {type: 'guild'});
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
name: groupName,
|
||||||
|
type: groupType,
|
||||||
|
privacy: 'private',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
guild = group;
|
||||||
|
user = groupLeader;
|
||||||
|
manager = members[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when group is not found', async () => {
|
it('returns error when group is not found', async () => {
|
||||||
@@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
|
|||||||
expect(task.everyX).to.eql(5);
|
expect(task.everyX).to.eql(5);
|
||||||
expect(new Date(task.startDate)).to.eql(now);
|
expect(new Date(task.startDate)).to.eql(now);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a manager to add a group task', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: manager._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = await manager.post(`/tasks/group/${guild._id}`, {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
up: false,
|
||||||
|
down: true,
|
||||||
|
notes: 1976,
|
||||||
|
});
|
||||||
|
|
||||||
|
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
|
||||||
|
|
||||||
|
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||||
|
expect(task.text).to.eql('test habit');
|
||||||
|
expect(task.notes).to.eql('1976');
|
||||||
|
expect(task.type).to.eql('habit');
|
||||||
|
expect(task.up).to.eql(false);
|
||||||
|
expect(task.down).to.eql(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
import { find } from 'lodash';
|
import { find } from 'lodash';
|
||||||
|
|
||||||
describe('POST /tasks/:taskId', () => {
|
describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||||
let user, guild, member, member2, task;
|
let user, guild, member, member2, task;
|
||||||
|
|
||||||
function findAssignedTask (memberTask) {
|
function findAssignedTask (memberTask) {
|
||||||
@@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
|
|||||||
expect(member1SyncedTask).to.exist;
|
expect(member1SyncedTask).to.exist;
|
||||||
expect(member2SyncedTask).to.exist;
|
expect(member2SyncedTask).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a manager to assign tasks', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
|
||||||
|
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||||
|
expect(syncedTask).to.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
|||||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||||
expect(member2SyncedTask).to.exist;
|
expect(member2SyncedTask).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows a manager to unassign a user from a task', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||||
|
|
||||||
|
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||||
|
expect(syncedTask).to.not.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
|
|||||||
expect(member2SyncedTask.up).to.eql(false);
|
expect(member2SyncedTask.up).to.eql(false);
|
||||||
expect(member2SyncedTask.down).to.eql(false);
|
expect(member2SyncedTask.down).to.eql(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updates the linked tasks', async () => {
|
||||||
|
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||||
|
managerId: member2._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await member2.put(`/tasks/${task._id}`, {
|
||||||
|
text: 'some new text',
|
||||||
|
up: false,
|
||||||
|
down: false,
|
||||||
|
notes: 'some new notes',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
expect(syncedTask.text).to.eql('some new text');
|
||||||
|
expect(syncedTask.up).to.eql(false);
|
||||||
|
expect(syncedTask.down).to.eql(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
sha1Encrypt as sha1EncryptPassword,
|
sha1Encrypt as sha1EncryptPassword,
|
||||||
} from '../../../../../../website/server/libs/password';
|
} from '../../../../../../website/server/libs/password';
|
||||||
|
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
describe('POST /user/auth/local/login', () => {
|
describe('POST /user/auth/local/login', () => {
|
||||||
let api;
|
let api;
|
||||||
let user;
|
let user;
|
||||||
@@ -43,7 +45,7 @@ describe('POST /user/auth/local/login', () => {
|
|||||||
})).to.eventually.be.rejected.and.eql({
|
})).to.eventually.be.rejected.and.eql({
|
||||||
code: 401,
|
code: 401,
|
||||||
error: 'NotAuthorized',
|
error: 'NotAuthorized',
|
||||||
message: t('accountSuspended', { userId: user._id }),
|
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,25 +34,209 @@ describe('POST /user/auth/local/register', () => {
|
|||||||
expect(user.apiToken).to.exist;
|
expect(user.apiToken).to.exist;
|
||||||
expect(user.auth.local.username).to.eql(username);
|
expect(user.auth.local.username).to.eql(username);
|
||||||
expect(user.profile.name).to.eql(username);
|
expect(user.profile.name).to.eql(username);
|
||||||
|
expect(user.newUser).to.eql(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provides default tags and tasks', async () => {
|
context('provides default tags and tasks', async () => {
|
||||||
let username = generateRandomUserName();
|
it('for a generic API consumer', async () => {
|
||||||
let email = `${username}@example.com`;
|
let username = generateRandomUserName();
|
||||||
let password = 'password';
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
let user = await api.post('/user/auth/local/register', {
|
let user = await api.post('/user/auth/local/register', {
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
confirmPassword: password,
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(todos).to.have.a.lengthOf(1);
|
||||||
|
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
|
||||||
|
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
|
||||||
|
|
||||||
|
expect(rewards).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(user.tags).to.have.a.lengthOf(7);
|
it('for Web', async () => {
|
||||||
expect(user.tasksOrder.todos).to.have.a.lengthOf(1);
|
api = requester(
|
||||||
expect(user.tasksOrder.dailys).to.have.a.lengthOf(0);
|
null,
|
||||||
expect(user.tasksOrder.rewards).to.have.a.lengthOf(0);
|
{'x-client': 'habitica-web'},
|
||||||
expect(user.tasksOrder.habits).to.have.a.lengthOf(0);
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(3);
|
||||||
|
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
|
||||||
|
expect(habits[0].notes).to.eql('');
|
||||||
|
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
|
||||||
|
expect(habits[1].notes).to.eql('');
|
||||||
|
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
|
||||||
|
expect(habits[2].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(todos).to.have.a.lengthOf(1);
|
||||||
|
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
|
||||||
|
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
|
||||||
|
|
||||||
|
expect(rewards).to.have.a.lengthOf(1);
|
||||||
|
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
|
||||||
|
expect(rewards[0].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for Android', async () => {
|
||||||
|
api = requester(
|
||||||
|
null,
|
||||||
|
{'x-client': 'habitica-android'},
|
||||||
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(2);
|
||||||
|
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
|
||||||
|
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
|
||||||
|
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
|
||||||
|
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
|
||||||
|
|
||||||
|
expect(dailys).to.have.a.lengthOf(1);
|
||||||
|
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
|
||||||
|
expect(dailys[0].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(todos).to.have.a.lengthOf(2);
|
||||||
|
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
|
||||||
|
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
|
||||||
|
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
|
||||||
|
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
|
||||||
|
|
||||||
|
expect(rewards).to.have.a.lengthOf(1);
|
||||||
|
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
|
||||||
|
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for iOS', async () => {
|
||||||
|
api = requester(
|
||||||
|
null,
|
||||||
|
{'x-client': 'habitica-ios'},
|
||||||
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(2);
|
||||||
|
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
|
||||||
|
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
|
||||||
|
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
|
||||||
|
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
|
||||||
|
|
||||||
|
expect(dailys).to.have.a.lengthOf(1);
|
||||||
|
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
|
||||||
|
expect(dailys[0].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(todos).to.have.a.lengthOf(2);
|
||||||
|
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
|
||||||
|
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
|
||||||
|
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
|
||||||
|
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
|
||||||
|
|
||||||
|
expect(rewards).to.have.a.lengthOf(1);
|
||||||
|
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
|
||||||
|
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('enrolls new users in an A/B test', async () => {
|
it('enrolls new users in an A/B test', async () => {
|
||||||
@@ -71,6 +255,23 @@ describe('POST /user/auth/local/register', () => {
|
|||||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes items awarded by default when creating a new user', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.items.quests.dustbunnies).to.equal(1);
|
||||||
|
expect(user.purchased.background.violet).to.be.ok;
|
||||||
|
expect(user.preferences.background).to.equal('violet');
|
||||||
|
});
|
||||||
|
|
||||||
it('requires password and confirmPassword to match', async () => {
|
it('requires password and confirmPassword to match', async () => {
|
||||||
let username = generateRandomUserName();
|
let username = generateRandomUserName();
|
||||||
let email = `${username}@example.com`;
|
let email = `${username}@example.com`;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
sha1Encrypt as sha1EncryptPassword,
|
sha1Encrypt as sha1EncryptPassword,
|
||||||
} from '../../../../../../website/server/libs/password';
|
} from '../../../../../../website/server/libs/password';
|
||||||
|
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
const ENDPOINT = '/user/auth/update-email';
|
const ENDPOINT = '/user/auth/update-email';
|
||||||
|
|
||||||
describe('PUT /user/auth/update-email', () => {
|
describe('PUT /user/auth/update-email', () => {
|
||||||
@@ -68,7 +70,7 @@ describe('PUT /user/auth/update-email', () => {
|
|||||||
})).to.eventually.be.rejected.and.eql({
|
})).to.eventually.be.rejected.and.eql({
|
||||||
code: 401,
|
code: 401,
|
||||||
error: 'NotAuthorized',
|
error: 'NotAuthorized',
|
||||||
message: t('cannotFulfillReq'),
|
message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL') }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ describe('payments/index', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('awards the Royal Purple Jackalope pet', async () => {
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
|
});
|
||||||
|
|
||||||
it('adds extra months to an existing subscription', async () => {
|
it('adds extra months to an existing subscription', async () => {
|
||||||
recipient.purchased.plan = plan;
|
recipient.purchased.plan = plan;
|
||||||
|
|
||||||
@@ -241,6 +247,12 @@ describe('payments/index', () => {
|
|||||||
expect(user.purchased.plan.dateCreated).to.exist;
|
expect(user.purchased.plan.dateCreated).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('awards the Royal Purple Jackalope pet', async () => {
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
|
});
|
||||||
|
|
||||||
it('sets extraMonths if plan has dateTerminated date', async () => {
|
it('sets extraMonths if plan has dateTerminated date', async () => {
|
||||||
user.purchased.plan = plan;
|
user.purchased.plan = plan;
|
||||||
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||||
@@ -633,5 +645,13 @@ describe('payments/index', () => {
|
|||||||
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
|
||||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('awards the Royal Purple Jackalope pet', async () => {
|
||||||
|
await api.addSubToGroupUser(user, group);
|
||||||
|
|
||||||
|
let updatedUser = await User.findById(user._id).exec();
|
||||||
|
|
||||||
|
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -603,6 +603,64 @@ describe('Purchasing a subscription for group', () => {
|
|||||||
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not modify a user with a Google subscription', async () => {
|
||||||
|
plan.customerId = 'random';
|
||||||
|
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||||
|
|
||||||
|
let recipient = new User();
|
||||||
|
recipient.profile.name = 'recipient';
|
||||||
|
recipient.purchased.plan = plan;
|
||||||
|
recipient.guilds.push(group._id);
|
||||||
|
await recipient.save();
|
||||||
|
|
||||||
|
user.guilds.push(group._id);
|
||||||
|
await user.save();
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedUser = await User.findById(recipient._id).exec();
|
||||||
|
|
||||||
|
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
|
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||||
|
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||||
|
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||||
|
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
|
||||||
|
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||||
|
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||||
|
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not modify a user with an iOS subscription', async () => {
|
||||||
|
plan.customerId = 'random';
|
||||||
|
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||||
|
|
||||||
|
let recipient = new User();
|
||||||
|
recipient.profile.name = 'recipient';
|
||||||
|
recipient.purchased.plan = plan;
|
||||||
|
recipient.guilds.push(group._id);
|
||||||
|
await recipient.save();
|
||||||
|
|
||||||
|
user.guilds.push(group._id);
|
||||||
|
await user.save();
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedUser = await User.findById(recipient._id).exec();
|
||||||
|
|
||||||
|
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
|
||||||
|
expect(updatedUser.purchased.plan.customerId).to.eql('random');
|
||||||
|
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
|
||||||
|
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
|
||||||
|
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
|
||||||
|
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
|
||||||
|
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
|
||||||
|
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
|
||||||
|
expect(updatedUser.purchased.plan.dateCreated).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
it('updates a user with a cancelled but active group subscription', async () => {
|
it('updates a user with a cancelled but active group subscription', async () => {
|
||||||
plan.key = 'basic_earned';
|
plan.key = 'basic_earned';
|
||||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { model as Coupon } from '../../../../../website/server/models/coupon';
|
|||||||
import stripePayments from '../../../../../website/server/libs/stripePayments';
|
import stripePayments from '../../../../../website/server/libs/stripePayments';
|
||||||
import payments from '../../../../../website/server/libs/payments';
|
import payments from '../../../../../website/server/libs/payments';
|
||||||
import common from '../../../../../website/common';
|
import common from '../../../../../website/common';
|
||||||
|
import logger from '../../../../../website/server/libs/logger';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
const i18n = common.i18n;
|
const i18n = common.i18n;
|
||||||
|
|
||||||
@@ -759,4 +762,245 @@ describe('Stripe Payments', () => {
|
|||||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleWebhooks', () => {
|
||||||
|
describe('all events', () => {
|
||||||
|
const eventType = 'account.updated';
|
||||||
|
const event = {id: 123};
|
||||||
|
const eventRetrieved = {type: eventType};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
|
||||||
|
sinon.stub(logger, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
logger.error.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs an error if an unsupported webhook event is passed', async () => {
|
||||||
|
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
|
||||||
|
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||||
|
expect(logger.error).to.have.been.called.once;
|
||||||
|
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves and validates the event from Stripe', async () => {
|
||||||
|
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||||
|
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||||
|
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('customer.subscription.deleted', () => {
|
||||||
|
const eventType = 'customer.subscription.deleted';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||||
|
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stripe.customers.del.restore();
|
||||||
|
payments.cancelSubscription.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
request: 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||||
|
|
||||||
|
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||||
|
expect(stripe.customers.del).to.not.have.been.called;
|
||||||
|
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('user subscription', () => {
|
||||||
|
it('throws an error if the user is not found', async () => {
|
||||||
|
const customerId = 456;
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
plan: {
|
||||||
|
id: 'basic_earned',
|
||||||
|
},
|
||||||
|
customer: customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||||
|
message: i18n.t('userNotFound'),
|
||||||
|
httpCode: 404,
|
||||||
|
name: 'NotFound',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripe.customers.del).to.not.have.been.called;
|
||||||
|
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||||
|
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||||
|
const customerId = '456';
|
||||||
|
|
||||||
|
let subscriber = new User();
|
||||||
|
subscriber.purchased.plan.customerId = customerId;
|
||||||
|
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||||
|
await subscriber.save();
|
||||||
|
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
plan: {
|
||||||
|
id: 'basic_earned',
|
||||||
|
},
|
||||||
|
customer: customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||||
|
|
||||||
|
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||||
|
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||||
|
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||||
|
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
|
||||||
|
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||||
|
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||||
|
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
|
||||||
|
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('group plan subscription', () => {
|
||||||
|
it('throws an error if the group is not found', async () => {
|
||||||
|
const customerId = 456;
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
plan: {
|
||||||
|
id: 'group_monthly',
|
||||||
|
},
|
||||||
|
customer: customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||||
|
message: i18n.t('groupNotFound'),
|
||||||
|
httpCode: 404,
|
||||||
|
name: 'NotFound',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripe.customers.del).to.not.have.been.called;
|
||||||
|
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||||
|
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the group leader is not found', async () => {
|
||||||
|
const customerId = 456;
|
||||||
|
|
||||||
|
let subscriber = generateGroup({
|
||||||
|
name: 'test group',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
leader: uuid(),
|
||||||
|
});
|
||||||
|
subscriber.purchased.plan.customerId = customerId;
|
||||||
|
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||||
|
await subscriber.save();
|
||||||
|
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
plan: {
|
||||||
|
id: 'group_monthly',
|
||||||
|
},
|
||||||
|
customer: customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||||
|
message: i18n.t('userNotFound'),
|
||||||
|
httpCode: 404,
|
||||||
|
name: 'NotFound',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripe.customers.del).to.not.have.been.called;
|
||||||
|
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||||
|
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||||
|
const customerId = '456';
|
||||||
|
|
||||||
|
let leader = new User();
|
||||||
|
await leader.save();
|
||||||
|
|
||||||
|
let subscriber = generateGroup({
|
||||||
|
name: 'test group',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
leader: leader._id,
|
||||||
|
});
|
||||||
|
subscriber.purchased.plan.customerId = customerId;
|
||||||
|
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||||
|
await subscriber.save();
|
||||||
|
|
||||||
|
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||||
|
id: 123,
|
||||||
|
type: eventType,
|
||||||
|
data: {
|
||||||
|
object: {
|
||||||
|
plan: {
|
||||||
|
id: 'group_monthly',
|
||||||
|
},
|
||||||
|
customer: customerId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||||
|
|
||||||
|
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||||
|
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||||
|
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||||
|
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
|
||||||
|
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||||
|
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||||
|
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
|
||||||
|
|
||||||
|
stripe.events.retrieve.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
182
test/api/v3/unit/middlewares/redirects.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateNext,
|
||||||
|
} from '../../../../helpers/api-unit.helper';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
|
|
||||||
|
describe('redirects middleware', () => {
|
||||||
|
let res, req, next;
|
||||||
|
let pathToRedirectsMiddleware = '../../../../../website/server/middlewares/redirects';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
res = generateRes();
|
||||||
|
req = generateReq();
|
||||||
|
next = generateNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('forceSSL', () => {
|
||||||
|
it('sends http requests to https', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceSSL(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.calledOnce;
|
||||||
|
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect https forwarded requests', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('https');
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceSSL(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect outside of production environments', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(false);
|
||||||
|
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceSSL(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect if base URL is not https', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('http://habitica.com');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceSSL(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('forceHabitica', () => {
|
||||||
|
it('sends requests with differing hostname to base URL host', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.hostname = 'www.habitica.com';
|
||||||
|
req.method = 'GET';
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
req.url = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.calledOnce;
|
||||||
|
expect(res.redirect).to.be.calledWith(301, 'https://habitica.com/static/front');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect outside of production environments', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(false);
|
||||||
|
req.hostname = 'www.habitica.com';
|
||||||
|
req.method = 'GET';
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
req.url = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect if env is set to ignore redirection', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('true');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.hostname = 'www.habitica.com';
|
||||||
|
req.method = 'GET';
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
req.url = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect if request hostname matches base URL host', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.hostname = 'habitica.com';
|
||||||
|
req.method = 'GET';
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
req.url = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect if request is an API URL', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.hostname = 'www.habitica.com';
|
||||||
|
req.method = 'GET';
|
||||||
|
req.originalUrl = '/api/v3/challenges';
|
||||||
|
req.url = '/api/v3/challenges';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not redirect if request method is not GET', () => {
|
||||||
|
let nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
|
||||||
|
nconfStub.withArgs('IS_PROD').returns(true);
|
||||||
|
req.hostname = 'www.habitica.com';
|
||||||
|
req.method = 'POST';
|
||||||
|
req.originalUrl = '/static/front';
|
||||||
|
req.url = '/static/front';
|
||||||
|
|
||||||
|
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
|
||||||
|
|
||||||
|
attachRedirects.forceHabitica(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect).to.be.notCalled;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -104,6 +104,40 @@ describe('Challenge Model', () => {
|
|||||||
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
||||||
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
||||||
expect(syncedTask).to.exist;
|
expect(syncedTask).to.exist;
|
||||||
|
expect(syncedTask.attribute).to.eql('str');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs a challenge to a user with the existing task', async () => {
|
||||||
|
await challenge.addTasks([task]);
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
|
||||||
|
return updatedLeadersTask.challenge.taskId === task._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
let createdAtBefore = syncedTask.createdAt;
|
||||||
|
let attributeBefore = syncedTask.attribute;
|
||||||
|
|
||||||
|
let newTitle = 'newName';
|
||||||
|
task.text = newTitle;
|
||||||
|
task.attribute = 'int';
|
||||||
|
await task.save();
|
||||||
|
await challenge.syncToUser(leader);
|
||||||
|
|
||||||
|
updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
|
||||||
|
syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
|
||||||
|
return updatedLeadersTask.challenge.taskId === task._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
let createdAtAfter = syncedTask.createdAt;
|
||||||
|
let attributeAfter = syncedTask.attribute;
|
||||||
|
|
||||||
|
expect(createdAtBefore).to.eql(createdAtAfter);
|
||||||
|
expect(attributeBefore).to.eql(attributeAfter);
|
||||||
|
expect(syncedTask.text).to.eql(newTitle);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates tasks to challenge and challenge members', async () => {
|
it('updates tasks to challenge and challenge members', async () => {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import moment from 'moment';
|
|||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
|
||||||
import {
|
import {
|
||||||
BadRequest,
|
SPAM_MESSAGE_LIMIT,
|
||||||
} from '../../../../../website/server/libs/errors';
|
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||||
|
SPAM_WINDOW_LENGTH,
|
||||||
|
INVITES_LIMIT,
|
||||||
|
model as Group,
|
||||||
|
} from '../../../../../website/server/models/group';
|
||||||
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||||
import * as email from '../../../../../website/server/libs/email';
|
import * as email from '../../../../../website/server/libs/email';
|
||||||
@@ -460,73 +463,67 @@ describe('Group Model', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if no uuids or emails are passed in', (done) => {
|
it('throws an error if no uuids or emails are passed in', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations(null, null, res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
|
it('throws an error if only uuids are passed in, but they are not an array', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations({ uuid: 'user-id'}, null, res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if only emails are passed in, but they are not an array', (done) => {
|
it('throws an error if only emails are passed in, but they are not an array', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
|
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations([], null, res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
|
it('throws an error if uuids are not passed in, and email array is empty', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations(null, [], res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
|
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
|
||||||
try {
|
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations([], [], res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if total invites exceed max invite constant', (done) => {
|
it('throws an error if total invites exceed max invite constant', async () => {
|
||||||
let uuids = [];
|
let uuids = [];
|
||||||
let emails = [];
|
let emails = [];
|
||||||
|
|
||||||
@@ -537,17 +534,16 @@ describe('Group Model', () => {
|
|||||||
|
|
||||||
uuids.push('one-more-uuid'); // to put it over the limit
|
uuids.push('one-more-uuid'); // to put it over the limit
|
||||||
|
|
||||||
try {
|
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
|
||||||
Group.validateInvitations(uuids, emails, res);
|
httpCode: 400,
|
||||||
} catch (err) {
|
message: 'Bad request.',
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
name: 'BadRequest',
|
||||||
expect(res.t).to.be.calledOnce;
|
});
|
||||||
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
expect(res.t).to.be.calledOnce;
|
||||||
done();
|
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw error if number of invites matches max invite limit', () => {
|
it('does not throw error if number of invites matches max invite limit', async () => {
|
||||||
let uuids = [];
|
let uuids = [];
|
||||||
let emails = [];
|
let emails = [];
|
||||||
|
|
||||||
@@ -556,49 +552,33 @@ describe('Group Model', () => {
|
|||||||
emails.push(`user-${i}@example.com`);
|
emails.push(`user-${i}@example.com`);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(function () {
|
await Group.validateInvitations(uuids, emails, res);
|
||||||
Group.validateInvitations(uuids, emails, res);
|
|
||||||
}).to.not.throw();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('does not throw an error if only user ids are passed in', () => {
|
|
||||||
expect(function () {
|
|
||||||
Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
|
||||||
}).to.not.throw();
|
|
||||||
|
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if only emails are passed in', () => {
|
|
||||||
expect(function () {
|
|
||||||
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
|
||||||
}).to.not.throw();
|
|
||||||
|
|
||||||
|
it('does not throw an error if only user ids are passed in', async () => {
|
||||||
|
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if both uuids and emails are passed in', () => {
|
it('does not throw an error if only emails are passed in', async () => {
|
||||||
expect(function () {
|
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
||||||
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
|
||||||
}).to.not.throw();
|
|
||||||
|
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
|
it('does not throw an error if both uuids and emails are passed in', async () => {
|
||||||
expect(function () {
|
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
||||||
Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
|
||||||
}).to.not.throw();
|
|
||||||
|
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
|
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
|
||||||
expect(function () {
|
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
||||||
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
expect(res.t).to.not.be.called;
|
||||||
}).to.not.throw();
|
});
|
||||||
|
|
||||||
|
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
|
||||||
|
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -621,6 +601,99 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#checkChatSpam', () => {
|
||||||
|
let testUser, testTime, tavern;
|
||||||
|
let testUserID = '1';
|
||||||
|
beforeEach(async () => {
|
||||||
|
testTime = Date.now();
|
||||||
|
|
||||||
|
tavern = new Group({
|
||||||
|
name: 'test tavern',
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
});
|
||||||
|
tavern._id = TAVERN_ID;
|
||||||
|
|
||||||
|
testUser = {
|
||||||
|
_id: testUserID,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateTestMessage (overrides = {}) {
|
||||||
|
return Object.assign({}, {
|
||||||
|
text: 'test message',
|
||||||
|
uuid: testUserID,
|
||||||
|
timestamp: testTime,
|
||||||
|
}, overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('group that is not the tavern returns false, while tavern returns true', async () => {
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
party.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
expect(party.checkChatSpam(testUser)).to.eql(false);
|
||||||
|
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('high enough contributor returns false', async () => {
|
||||||
|
let highContributor = testUser;
|
||||||
|
highContributor.contributor = {
|
||||||
|
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chat with no messages returns false', async () => {
|
||||||
|
expect(tavern.chat.length).to.eql(0);
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user has not reached limit but another one has returns false', async () => {
|
||||||
|
let otherUserID = '2';
|
||||||
|
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user messages is less than the limit returns false', async () => {
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user has reached the message limit outside of window returns false', async () => {
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
|
||||||
|
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
|
||||||
|
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user has posted too many messages in limit returns true', async () => {
|
||||||
|
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||||
|
tavern.chat.push(generateTestMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tavern.checkChatSpam(testUser)).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#leaveGroup', () => {
|
describe('#leaveGroup', () => {
|
||||||
it('removes user from group quest', async () => {
|
it('removes user from group quest', async () => {
|
||||||
party.quest.members = {
|
party.quest.members = {
|
||||||
|
|||||||
@@ -334,4 +334,34 @@ describe('Settings Controller', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Fixing character values', function () {
|
||||||
|
describe('#restore', function () {
|
||||||
|
var blankRestoreValues = {
|
||||||
|
stats: {
|
||||||
|
hp: 0,
|
||||||
|
exp: 0,
|
||||||
|
gp: 0,
|
||||||
|
lvl: 0,
|
||||||
|
mp: 0,
|
||||||
|
},
|
||||||
|
achievements: {
|
||||||
|
streak: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('doesn\'t update character values when level is less than 1', function () {
|
||||||
|
scope.restoreValues = blankRestoreValues;
|
||||||
|
scope.restore();
|
||||||
|
expect(User.set).to.not.be.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates character values when level is at least 1', function () {
|
||||||
|
scope.restoreValues = blankRestoreValues;
|
||||||
|
scope.restoreValues.stats.lvl = 1;
|
||||||
|
scope.restore();
|
||||||
|
expect(User.set).to.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
69
test/client-old/spec/controllers/userCtrlSpec.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('User Controller', function() {
|
||||||
|
var $rootScope, $window, User, shared, scope, ctrl, content;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
module(function ($provide) {
|
||||||
|
var user = specHelper.newUser();
|
||||||
|
User = {user: user}
|
||||||
|
$provide.value('Guide', sandbox.stub());
|
||||||
|
$provide.value('User', User);
|
||||||
|
$provide.value('Achievement', sandbox.stub());
|
||||||
|
$provide.value('Social', sandbox.stub());
|
||||||
|
$provide.value('Shared', {
|
||||||
|
achievements: {
|
||||||
|
getAchievementsForProfile: sandbox.stub()
|
||||||
|
},
|
||||||
|
shops: {
|
||||||
|
getBackgroundShopSets: sandbox.stub()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$provide.value('Content', {
|
||||||
|
loginIncentives: sandbox.stub()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
inject(function($rootScope, $controller, User, Content) {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
content = Content;
|
||||||
|
$controller('RootCtrl', { $scope: scope, User: User});
|
||||||
|
ctrl = $controller('UserCtrl', { $scope: scope, User: User, $window: $window});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getProgressDisplay', function() {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.stub(window.env, 't');
|
||||||
|
window.env.t.onFirstCall().returns('Progress until next');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return initial progress', function() {
|
||||||
|
scope.profile.loginIncentives = 0;
|
||||||
|
content.loginIncentives = [{
|
||||||
|
nextRewardAt: 1,
|
||||||
|
reward: true
|
||||||
|
}];
|
||||||
|
var actual = scope.getProgressDisplay();
|
||||||
|
expect(actual.trim()).to.eql('Progress until next 0/1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return progress between next reward and current reward', function() {
|
||||||
|
scope.profile.loginIncentives = 1;
|
||||||
|
content.loginIncentives = [{
|
||||||
|
nextRewardAt: 1,
|
||||||
|
reward: true
|
||||||
|
}, {
|
||||||
|
prevRewardAt: 0,
|
||||||
|
nextRewardAt: 2,
|
||||||
|
reward: true
|
||||||
|
}, {
|
||||||
|
prevRewardAt: 1,
|
||||||
|
nextRewardAt: 3
|
||||||
|
}];
|
||||||
|
var actual = scope.getProgressDisplay();
|
||||||
|
expect(actual.trim()).to.eql('Progress until next 0/1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
test/client-old/spec/directives/task.directive.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
describe('task Directive', () => {
|
||||||
|
var compile, scope, directiveElem, $modal;
|
||||||
|
|
||||||
|
beforeEach(function(){
|
||||||
|
module(function($provide) {
|
||||||
|
$modal = {
|
||||||
|
open: sandbox.spy(),
|
||||||
|
};
|
||||||
|
$provide.value('$modal', $modal);
|
||||||
|
});
|
||||||
|
|
||||||
|
inject(function($compile, $rootScope, $templateCache) {
|
||||||
|
compile = $compile;
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
|
||||||
|
$templateCache.put('templates/task.html', '<div>Task</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
directiveElem = getCompiledElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCompiledElement(){
|
||||||
|
var element = angular.element('<task></task>');
|
||||||
|
var compiledElement = compile(element)(scope);
|
||||||
|
scope.$digest();
|
||||||
|
return compiledElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
xit('opens task note modal', () => {
|
||||||
|
scope.showNoteDetails();
|
||||||
|
|
||||||
|
expect($modal.open).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -81,8 +81,11 @@ module.exports = function karmaConfig (config) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
coverageReporter: {
|
coverageReporter: {
|
||||||
type: 'lcov',
|
reporters: [
|
||||||
dir: 'coverage/karma',
|
{ type: 'lcov', subdir: '.' },
|
||||||
|
{ type: 'text-summary' },
|
||||||
|
],
|
||||||
|
dir: '../../../coverage/karma',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Enable mocha-style reporting, for better test visibility
|
// Enable mocha-style reporting, for better test visibility
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module.exports = function (config) {
|
|||||||
noInfo: true,
|
noInfo: true,
|
||||||
},
|
},
|
||||||
coverageReporter: {
|
coverageReporter: {
|
||||||
dir: './coverage',
|
dir: '../../../coverage/client-unit',
|
||||||
reporters: [
|
reporters: [
|
||||||
{ type: 'lcov', subdir: '.' },
|
{ type: 'lcov', subdir: '.' },
|
||||||
{ type: 'text-summary' },
|
{ type: 'text-summary' },
|
||||||
|
|||||||
151
test/client/unit/specs/libs/asyncResource.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { asyncResourceFactory, loadAsyncResource } from 'client/libs/asyncResource';
|
||||||
|
import axios from 'axios';
|
||||||
|
import generateStore from 'client/store';
|
||||||
|
import { sleep } from '../../../../helpers/sleep';
|
||||||
|
|
||||||
|
describe('async resource', () => {
|
||||||
|
it('asyncResourceFactory', () => {
|
||||||
|
const resource = asyncResourceFactory();
|
||||||
|
expect(resource.loadingStatus).to.equal('NOT_LOADED');
|
||||||
|
expect(resource.data).to.equal(null);
|
||||||
|
expect(resource).to.not.equal(asyncResourceFactory);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadAsyncResource', () => {
|
||||||
|
context('errors', () => {
|
||||||
|
it('store is missing', () => {
|
||||||
|
expect(() => loadAsyncResource({})).to.throw;
|
||||||
|
});
|
||||||
|
it('path is missing', () => {
|
||||||
|
expect(() => loadAsyncResource({
|
||||||
|
store: 'store',
|
||||||
|
})).to.throw;
|
||||||
|
});
|
||||||
|
it('url is missing', () => {
|
||||||
|
expect(() => loadAsyncResource({
|
||||||
|
store: 'store',
|
||||||
|
path: 'path',
|
||||||
|
})).to.throw;
|
||||||
|
});
|
||||||
|
it('deserialize is missing', () => {
|
||||||
|
expect(() => loadAsyncResource({
|
||||||
|
store: 'store',
|
||||||
|
path: 'path',
|
||||||
|
url: 'url',
|
||||||
|
})).to.throw;
|
||||||
|
});
|
||||||
|
it('resource not found', () => {
|
||||||
|
const store = generateStore();
|
||||||
|
|
||||||
|
expect(() => loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'not existing path',
|
||||||
|
url: 'url',
|
||||||
|
deserialize: 'deserialize',
|
||||||
|
})).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid loading status', () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user.loadingStatus = 'INVALID';
|
||||||
|
|
||||||
|
expect(loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'user',
|
||||||
|
url: 'url',
|
||||||
|
deserialize: 'deserialize',
|
||||||
|
})).to.eventually.be.rejected;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the resource if it is already loaded and forceLoad is false', async () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user.loadingStatus = 'LOADED';
|
||||||
|
store.state.user.data = {_id: 1};
|
||||||
|
|
||||||
|
sandbox.stub(axios, 'get');
|
||||||
|
|
||||||
|
const resource = await loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'user',
|
||||||
|
url: 'url',
|
||||||
|
deserialize: 'deserialize',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resource).to.equal(store.state.user);
|
||||||
|
expect(axios.get).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load the resource if it is not loaded', async () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user = asyncResourceFactory();
|
||||||
|
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||||
|
|
||||||
|
const resource = await loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'user',
|
||||||
|
url: '/api/v3/user',
|
||||||
|
deserialize (response) {
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resource).to.equal(store.state.user);
|
||||||
|
expect(resource.loadingStatus).to.equal('LOADED');
|
||||||
|
expect(resource.data._id).to.equal(1);
|
||||||
|
expect(axios.get).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('load the resource if it is loaded but forceLoad is true', async () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user.loadingStatus = 'LOADED';
|
||||||
|
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||||
|
|
||||||
|
const resource = await loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'user',
|
||||||
|
url: '/api/v3/user',
|
||||||
|
deserialize (response) {
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
forceLoad: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resource).to.equal(store.state.user);
|
||||||
|
expect(resource.loadingStatus).to.equal('LOADED');
|
||||||
|
expect(resource.data._id).to.equal(1);
|
||||||
|
expect(axios.get).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not send multiple requests if the resource is being loaded', async () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user.loadingStatus = 'LOADING';
|
||||||
|
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
|
||||||
|
|
||||||
|
const resourcePromise = loadAsyncResource({
|
||||||
|
store,
|
||||||
|
path: 'user',
|
||||||
|
url: '/api/v3/user',
|
||||||
|
deserialize (response) {
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
forceLoad: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(0.1);
|
||||||
|
const userData = {_id: 1};
|
||||||
|
|
||||||
|
expect(store.state.user.loadingStatus).to.equal('LOADING');
|
||||||
|
expect(axios.get).to.not.have.been.called;
|
||||||
|
store.state.user.data = userData;
|
||||||
|
store.state.user.loadingStatus = 'LOADED';
|
||||||
|
|
||||||
|
const result = await resourcePromise;
|
||||||
|
expect(axios.get).to.not.have.been.called;
|
||||||
|
expect(result).to.equal(store.state.user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import deepFreeze from 'client/libs/deepFreeze';
|
import deepFreeze from 'client/libs/deepFreeze';
|
||||||
|
|
||||||
describe('deepFreeze', () => {
|
describe('deepFreeze', () => {
|
||||||
it('works as expected', () => {
|
it('deeply freezes an object', () => {
|
||||||
let obj = {
|
let obj = {
|
||||||
a: 1,
|
a: 1,
|
||||||
b () {
|
b () {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import i18n from 'client/plugins/i18n';
|
import i18n from 'client/libs/i18n';
|
||||||
import commoni18n from 'common/script/i18n';
|
import commoni18n from 'common/script/i18n';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
describe('i18n plugin', () => {
|
describe('i18n plugin', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
i18n.install(Vue);
|
Vue.use(i18n);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds $t to Vue.prototype', () => {
|
it('adds $t to Vue.prototype', () => {
|
||||||
178
test/client/unit/specs/libs/store.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import StoreModule, { mapState, mapGetters, mapActions } from 'client/libs/store';
|
||||||
|
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
|
||||||
|
|
||||||
|
describe('Store', () => {
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new StoreModule({ // eslint-disable-line babel/new-cap
|
||||||
|
state: {
|
||||||
|
name: 'test',
|
||||||
|
nested: {
|
||||||
|
name: 'nested state test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
computedName ({ state }) {
|
||||||
|
return `${state.name} computed!`;
|
||||||
|
},
|
||||||
|
...flattenAndNamespace({
|
||||||
|
nested: {
|
||||||
|
computedName ({ state }) {
|
||||||
|
return `${state.name} computed!`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
getName ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
...flattenAndNamespace({
|
||||||
|
nested: {
|
||||||
|
getName ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.use(StoreModule);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects itself in all component', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
store,
|
||||||
|
created () {
|
||||||
|
expect(this.$store).to.equal(store);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can watch a function on the state', (done) => {
|
||||||
|
store.watch(state => state.name, (newName) => {
|
||||||
|
expect(newName).to.equal('test updated');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
store.state.name = 'test updated';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getters', () => {
|
||||||
|
it('supports getters', () => {
|
||||||
|
expect(store.getters.computedName).to.equal('test computed!');
|
||||||
|
store.state.name = 'test updated';
|
||||||
|
expect(store.getters.computedName).to.equal('test updated computed!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports nested getters', () => {
|
||||||
|
expect(store.getters['nested:computedName']).to.equal('test computed!');
|
||||||
|
store.state.name = 'test updated';
|
||||||
|
expect(store.getters['nested:computedName']).to.equal('test updated computed!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('actions', () => {
|
||||||
|
it('can dispatch an action', () => {
|
||||||
|
expect(store.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can dispatch a nested action', () => {
|
||||||
|
expect(store.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error is the action doesn\'t exists', () => {
|
||||||
|
expect(() => store.dispatched('wrong')).to.throw;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('helpers', () => {
|
||||||
|
it('mapState', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
store,
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['name']),
|
||||||
|
...mapState({
|
||||||
|
nameComputed (state, getters) {
|
||||||
|
return `${this.title} ${getters.computedName} ${state.name}`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...mapState({nestedTest: 'nested.name'}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.name).to.equal('test');
|
||||||
|
expect(this.nameComputed).to.equal('internal test computed! test');
|
||||||
|
expect(this.nestedTest).to.equal('nested state test');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mapGetters', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
store,
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['computedName']),
|
||||||
|
...mapGetters({
|
||||||
|
nameComputedTwice: 'computedName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.computedName).to.equal('test computed!');
|
||||||
|
expect(this.nameComputedTwice).to.equal('test computed!');
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mapActions', (done) => {
|
||||||
|
new Vue({ // eslint-disable-line no-new
|
||||||
|
store,
|
||||||
|
data: {
|
||||||
|
title: 'internal',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['getName']),
|
||||||
|
...mapActions({
|
||||||
|
getNameRenamed: 'getName',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
expect(this.getName('123')).to.deep.equal(['test', '123']);
|
||||||
|
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flattenAndNamespace', () => {
|
||||||
|
let result = flattenAndNamespace({
|
||||||
|
nested: {
|
||||||
|
computed ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
getName ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nested2: {
|
||||||
|
getName ({ state }, ...args) {
|
||||||
|
return [state.name, ...args];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Object.keys(result).length).to.equal(3);
|
||||||
|
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { fetchAll as fetchAllGuilds } from 'client/store/actions/guilds';
|
|
||||||
import axios from 'axios';
|
|
||||||
import store from 'client/store';
|
|
||||||
|
|
||||||
describe('guilds actions', () => {
|
|
||||||
it('fetchAll', async () => {
|
|
||||||
const guilds = [{_id: 1}];
|
|
||||||
sandbox
|
|
||||||
.stub(axios, 'get')
|
|
||||||
.withArgs('/api/v3/groups?type=publicGuilds')
|
|
||||||
.returns(Promise.resolve({data: {data: guilds}}));
|
|
||||||
|
|
||||||
await fetchAllGuilds(store);
|
|
||||||
|
|
||||||
expect(store.state.guilds).to.equal(guilds);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,54 @@
|
|||||||
import { fetchUserTasks } from 'client/store/actions/tasks';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import store from 'client/store';
|
import generateStore from 'client/store';
|
||||||
|
|
||||||
describe('tasks actions', () => {
|
describe('tasks actions', () => {
|
||||||
it('fetchUserTasks', async () => {
|
let store;
|
||||||
const tasks = [{_id: 1}];
|
|
||||||
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
|
||||||
|
|
||||||
await fetchUserTasks(store);
|
beforeEach(() => {
|
||||||
|
store = generateStore();
|
||||||
|
});
|
||||||
|
|
||||||
expect(store.state.tasks).to.equal(tasks);
|
describe('fetchUserTasks', () => {
|
||||||
|
it('fetches user tasks', async () => {
|
||||||
|
expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED');
|
||||||
|
const tasks = [{_id: 1}];
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||||
|
|
||||||
|
await store.dispatch('tasks:fetchUserTasks');
|
||||||
|
|
||||||
|
expect(store.state.tasks.data).to.equal(tasks);
|
||||||
|
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reload tasks by default', async () => {
|
||||||
|
const originalTask = [{_id: 1}];
|
||||||
|
store.state.tasks = {
|
||||||
|
loadingStatus: 'LOADED',
|
||||||
|
data: originalTask,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tasks = [{_id: 2}];
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||||
|
|
||||||
|
await store.dispatch('tasks:fetchUserTasks');
|
||||||
|
|
||||||
|
expect(store.state.tasks.data).to.equal(originalTask);
|
||||||
|
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can reload tasks if forceLoad is true', async () => {
|
||||||
|
store.state.tasks = {
|
||||||
|
loadingStatus: 'LOADED',
|
||||||
|
data: [{_id: 1}],
|
||||||
|
};
|
||||||
|
|
||||||
|
const tasks = [{_id: 2}];
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
|
||||||
|
|
||||||
|
await store.dispatch('tasks:fetchUserTasks', true);
|
||||||
|
|
||||||
|
expect(store.state.tasks.data).to.equal(tasks);
|
||||||
|
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,14 +1,54 @@
|
|||||||
import { fetch as fetchUser } from 'client/store/actions/user';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import store from 'client/store';
|
import generateStore from 'client/store';
|
||||||
|
|
||||||
describe('user actions', () => {
|
describe('tasks actions', () => {
|
||||||
it('fetch', async () => {
|
let store;
|
||||||
const user = {_id: 1};
|
|
||||||
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
|
||||||
|
|
||||||
await fetchUser(store);
|
beforeEach(() => {
|
||||||
|
store = generateStore();
|
||||||
|
});
|
||||||
|
|
||||||
expect(store.state.user).to.equal(user);
|
describe('fetch', () => {
|
||||||
|
it('loads the user', async () => {
|
||||||
|
expect(store.state.user.loadingStatus).to.equal('NOT_LOADED');
|
||||||
|
const user = {_id: 1};
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||||
|
|
||||||
|
await store.dispatch('user:fetch');
|
||||||
|
|
||||||
|
expect(store.state.user.data).to.equal(user);
|
||||||
|
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reload user by default', async () => {
|
||||||
|
const originalUser = {_id: 1};
|
||||||
|
store.state.user = {
|
||||||
|
loadingStatus: 'LOADED',
|
||||||
|
data: originalUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = {_id: 2};
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||||
|
|
||||||
|
await store.dispatch('user:fetch');
|
||||||
|
|
||||||
|
expect(store.state.user.data).to.equal(originalUser);
|
||||||
|
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can reload user if forceLoad is true', async () => {
|
||||||
|
store.state.user = {
|
||||||
|
loadingStatus: 'LOADED',
|
||||||
|
data: {_id: 1},
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = {_id: 2};
|
||||||
|
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
|
||||||
|
|
||||||
|
await store.dispatch('user:fetch', true);
|
||||||
|
|
||||||
|
expect(store.state.user.data).to.equal(user);
|
||||||
|
expect(store.state.user.loadingStatus).to.equal('LOADED');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
16
test/client/unit/specs/store/getters/tasks/getTagsFor.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import generateStore from 'client/store';
|
||||||
|
|
||||||
|
describe('getTagsFor getter', () => {
|
||||||
|
it('returns the tags for a task', () => {
|
||||||
|
const store = generateStore();
|
||||||
|
store.state.user.data = {
|
||||||
|
tags: [
|
||||||
|
{id: 1, name: 'tag 1'},
|
||||||
|
{id: 2, name: 'tag 2'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const task = {tags: [2]};
|
||||||
|
expect(store.getters['tasks:getTagsFor'](task)).to.deep.equal(['tag 2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ describe('userGems getter', () => {
|
|||||||
expect(userGems({
|
expect(userGems({
|
||||||
state: {
|
state: {
|
||||||
user: {
|
user: {
|
||||||
balance: 4.5,
|
data: {balance: 4.5},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})).to.equal(18);
|
})).to.equal(18);
|
||||||
@@ -1,168 +1,8 @@
|
|||||||
import Vue from 'vue';
|
import generateStore from 'client/store';
|
||||||
import storeInjector from 'inject-loader?-vue!client/store';
|
import Store from 'client/libs/store';
|
||||||
import { mapState, mapGetters, mapActions } from 'client/store';
|
|
||||||
import { flattenAndNamespace } from 'client/store/helpers/internals';
|
|
||||||
|
|
||||||
describe('Store', () => {
|
describe('Application store', () => {
|
||||||
let injectedStore;
|
it('is an instance of Store', () => {
|
||||||
|
expect(generateStore()).to.be.an.instanceof(Store);
|
||||||
beforeEach(() => {
|
|
||||||
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
|
|
||||||
'./state': {
|
|
||||||
name: 'test',
|
|
||||||
},
|
|
||||||
'./getters': {
|
|
||||||
computedName ({ state }) {
|
|
||||||
return `${state.name} computed!`;
|
|
||||||
},
|
|
||||||
...flattenAndNamespace({
|
|
||||||
nested: {
|
|
||||||
computedName ({ state }) {
|
|
||||||
return `${state.name} computed!`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
'./actions': {
|
|
||||||
getName ({ state }, ...args) {
|
|
||||||
return [state.name, ...args];
|
|
||||||
},
|
|
||||||
...flattenAndNamespace({
|
|
||||||
nested: {
|
|
||||||
getName ({ state }, ...args) {
|
|
||||||
return [state.name, ...args];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}).default;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('injects itself in all component', (done) => {
|
|
||||||
new Vue({ // eslint-disable-line no-new
|
|
||||||
created () {
|
|
||||||
expect(this.$store).to.equal(injectedStore);
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can watch a function on the state', (done) => {
|
|
||||||
injectedStore.watch(state => state.name, (newName) => {
|
|
||||||
expect(newName).to.equal('test updated');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
injectedStore.state.name = 'test updated';
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getters', () => {
|
|
||||||
it('supports getters', () => {
|
|
||||||
expect(injectedStore.getters.computedName).to.equal('test computed!');
|
|
||||||
injectedStore.state.name = 'test updated';
|
|
||||||
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('supports nested getters', () => {
|
|
||||||
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
|
|
||||||
injectedStore.state.name = 'test updated';
|
|
||||||
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('actions', () => {
|
|
||||||
it('can dispatch an action', () => {
|
|
||||||
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can dispatch a nested action', () => {
|
|
||||||
expect(injectedStore.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error is the action doesn\'t exists', () => {
|
|
||||||
expect(() => injectedStore.dispatched('wrong')).to.throw;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('helpers', () => {
|
|
||||||
it('mapState', (done) => {
|
|
||||||
new Vue({ // eslint-disable-line no-new
|
|
||||||
data: {
|
|
||||||
title: 'internal',
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(['name']),
|
|
||||||
...mapState({
|
|
||||||
nameComputed (state, getters) {
|
|
||||||
return `${this.title} ${getters.computedName} ${state.name}`;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
expect(this.name).to.equal('test');
|
|
||||||
expect(this.nameComputed).to.equal('internal test computed! test');
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('mapGetters', (done) => {
|
|
||||||
new Vue({ // eslint-disable-line no-new
|
|
||||||
data: {
|
|
||||||
title: 'internal',
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters(['computedName']),
|
|
||||||
...mapGetters({
|
|
||||||
nameComputedTwice: 'computedName',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
expect(this.computedName).to.equal('test computed!');
|
|
||||||
expect(this.nameComputedTwice).to.equal('test computed!');
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('mapActions', (done) => {
|
|
||||||
new Vue({ // eslint-disable-line no-new
|
|
||||||
data: {
|
|
||||||
title: 'internal',
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(['getName']),
|
|
||||||
...mapActions({
|
|
||||||
getNameRenamed: 'getName',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
expect(this.getName('123')).to.deep.equal(['test', '123']);
|
|
||||||
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('flattenAndNamespace', () => {
|
|
||||||
let result = flattenAndNamespace({
|
|
||||||
nested: {
|
|
||||||
computed ({ state }, ...args) {
|
|
||||||
return [state.name, ...args];
|
|
||||||
},
|
|
||||||
getName ({ state }, ...args) {
|
|
||||||
return [state.name, ...args];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nested2: {
|
|
||||||
getName ({ state }, ...args) {
|
|
||||||
return [state.name, ...args];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Object.keys(result).length).to.equal(3);
|
|
||||||
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import changeClass from '../../../website/common/script/ops/changeClass';
|
import changeClass from '../../../website/common/script/ops/changeClass';
|
||||||
import {
|
import {
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
|
BadRequest,
|
||||||
} from '../../../website/common/script/libs/errors';
|
} from '../../../website/common/script/libs/errors';
|
||||||
import i18n from '../../../website/common/script/i18n';
|
import i18n from '../../../website/common/script/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,19 @@ describe('shared.ops.changeClass', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('req.query.class is an invalid class', (done) => {
|
||||||
|
user.flags.classSelected = false;
|
||||||
|
user.preferences.disableClasses = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
changeClass(user, {query: {class: 'cellist'}});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidClass'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
context('req.query.class is a valid class', () => {
|
context('req.query.class is a valid class', () => {
|
||||||
it('errors if user.stats.flagSelected is true and user.balance < 0.75', (done) => {
|
it('errors if user.stats.flagSelected is true and user.balance < 0.75', (done) => {
|
||||||
user.flags.classSelected = true;
|
user.flags.classSelected = true;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ describe('shouldDo', () => {
|
|||||||
let options = {};
|
let options = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
options = {};
|
||||||
day = new Date();
|
day = new Date();
|
||||||
dailyTask = {
|
dailyTask = {
|
||||||
completed: 'false',
|
completed: 'false',
|
||||||
@@ -38,6 +39,164 @@ describe('shouldDo', () => {
|
|||||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Timezone variations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dailyTask.frequency = 'daily';
|
||||||
|
});
|
||||||
|
|
||||||
|
context('User timezone is UTC', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.timezoneOffset = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is before today', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is today', () => {
|
||||||
|
dailyTask.startDate = moment().toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('User timezone is between UTC-12 and UTC (0~720)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.timezoneOffset = 600;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is before today', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is today', () => {
|
||||||
|
dailyTask.startDate = moment().startOf('day').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the user\'s current time is after start date and CDS', () => {
|
||||||
|
options.dayStart = 4;
|
||||||
|
day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate();
|
||||||
|
dailyTask.startDate = moment().utcOffset(options.timezoneOffset).subtract(1, 'day').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the user\'s current time is before CDS', () => {
|
||||||
|
options.dayStart = 8;
|
||||||
|
day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate();
|
||||||
|
dailyTask.startDate = moment().utcOffset(options.timezoneOffset).startOf('day').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('User timezone is between UTC and GMT+14 (-840~0)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.timezoneOffset = -420;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is before today', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if Start Date is today', () => {
|
||||||
|
dailyTask.startDate = moment().startOf('day').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if the user\'s current time is after CDS', () => {
|
||||||
|
options.dayStart = 4;
|
||||||
|
day = moment().utcOffset(options.timezoneOffset).startOf('day').add(6, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the user\'s current time is before CDS', () => {
|
||||||
|
options.dayStart = 8;
|
||||||
|
day = moment().utcOffset(options.timezoneOffset).startOf('day').add(2, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('CDS variations', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Daily is due every 2 days, and start today
|
||||||
|
dailyTask.frequency = 'daily';
|
||||||
|
dailyTask.everyX = 2;
|
||||||
|
dailyTask.startDate = new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('CDS is midnight (Default dayStart=0)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.dayStart = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is yesterday', () => {
|
||||||
|
it('should not be due yesterday', () => {
|
||||||
|
day = moment(day).subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is today', () => {
|
||||||
|
it('returns false if current time is before midnight', () => {
|
||||||
|
day = moment(day).startOf('day').subtract(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if current time is after midnight', () => {
|
||||||
|
day = moment(day).startOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is tomorrow', () => {
|
||||||
|
it('should not be due tomorrow', () => {
|
||||||
|
day = moment(day).add(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('CDS is 0 <= n < 24', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.dayStart = 7;
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is yesterday', () => {
|
||||||
|
it('should not be due yesterday', () => {
|
||||||
|
day = moment(day).subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is today', () => {
|
||||||
|
it('returns false if current hour is before CDS', () => {
|
||||||
|
day = moment(day).startOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if current hour is after CDS', () => {
|
||||||
|
day = moment(day).startOf('day').add(9, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is tomorrow', () => {
|
||||||
|
it('returns true if current hour is before CDS', () => {
|
||||||
|
day = moment(day).endOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if current hour is after CDS', () => {
|
||||||
|
day = moment(day).endOf('day').add(9, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
context('Every X Days', () => {
|
context('Every X Days', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dailyTask.frequency = 'daily';
|
dailyTask.frequency = 'daily';
|
||||||
@@ -56,17 +215,74 @@ describe('shouldDo', () => {
|
|||||||
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true on multiples of x', () => {
|
it('returns true on the Start Date', () => {
|
||||||
dailyTask.startDate = moment().subtract(7, 'days').toDate();
|
dailyTask.startDate = moment().toDate();
|
||||||
dailyTask.everyX = 7;
|
|
||||||
|
|
||||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
day = moment(day).add(7, 'days');
|
context('On multiples of x', () => {
|
||||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
it('returns true when CDS is midnight', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(7, 'days').toDate();
|
||||||
|
dailyTask.everyX = 7;
|
||||||
|
|
||||||
day = moment(day).add(7, 'days');
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
|
||||||
|
day = moment(day).add(7, 'days');
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
|
||||||
|
day = moment(day).add(7, 'days');
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when current time is after CDS', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(5, 'days').toDate();
|
||||||
|
dailyTask.everyX = 5;
|
||||||
|
|
||||||
|
options.dayStart = 3;
|
||||||
|
day = moment(day).startOf('day').add(8, 'hours').toDate();
|
||||||
|
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when current time is before CDS', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(5, 'days').toDate();
|
||||||
|
dailyTask.everyX = 5;
|
||||||
|
|
||||||
|
options.dayStart = 14;
|
||||||
|
day = moment(day).startOf('day').add(7, 'hours').toDate();
|
||||||
|
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('If number of X days is zero', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dailyTask.everyX = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on the Start Date', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(4, 'days').toDate();
|
||||||
|
day = moment().subtract(4, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on the day before Start Date', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(4, 'days').toDate();
|
||||||
|
day = moment().subtract(5, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false on the day after Start Date', () => {
|
||||||
|
dailyTask.startDate = moment().subtract(4, 'days').toDate();
|
||||||
|
day = moment().subtract(3, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for today', () => {
|
||||||
|
dailyTask.startDate = moment().toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,6 +350,135 @@ describe('shouldDo', () => {
|
|||||||
it('returns true if Daily on matching days of the week', () => {
|
it('returns true if Daily on matching days of the week', () => {
|
||||||
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('Day of the week matches', () => {
|
||||||
|
const weekdayMap = {
|
||||||
|
1: 'm',
|
||||||
|
2: 't',
|
||||||
|
3: 'w',
|
||||||
|
4: 'th',
|
||||||
|
5: 'f',
|
||||||
|
6: 's',
|
||||||
|
7: 'su',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Set repeat day to current weekday
|
||||||
|
const currentWeekday = moment().isoWeekday();
|
||||||
|
dailyTask.startDate = moment().startOf('day').toDate();
|
||||||
|
dailyTask.repeat = {
|
||||||
|
su: false,
|
||||||
|
s: false,
|
||||||
|
f: false,
|
||||||
|
th: false,
|
||||||
|
w: false,
|
||||||
|
t: false,
|
||||||
|
m: false,
|
||||||
|
};
|
||||||
|
dailyTask.repeat[weekdayMap[currentWeekday]] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
context('CDS is midnight (Default dayStart=0)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.dayStart = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is one day before the matching day', () => {
|
||||||
|
it('should return false', () => {
|
||||||
|
day = moment(day).subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is on the matching day', () => {
|
||||||
|
it('returns false if current time is before midnight', () => {
|
||||||
|
day = moment(day).startOf('day').subtract(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if current time is after midnight', () => {
|
||||||
|
day = moment(day).startOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is one day after the matching day', () => {
|
||||||
|
it('should not be due tomorrow', () => {
|
||||||
|
day = moment(day).add(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('CDS is 0 <= n < 24', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
options.dayStart = 7;
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is one day before the matching day', () => {
|
||||||
|
it('should not be due', () => {
|
||||||
|
day = moment(day).subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is on the matching day', () => {
|
||||||
|
it('returns false if current hour is before CDS', () => {
|
||||||
|
day = moment(day).startOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true if current hour is after CDS', () => {
|
||||||
|
day = moment(day).startOf('day').add(9, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Current Date is one day after the matching day', () => {
|
||||||
|
it('returns true if current hour is before CDS', () => {
|
||||||
|
day = moment(day).endOf('day').add(1, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if current hour is after CDS', () => {
|
||||||
|
day = moment(day).endOf('day').add(9, 'hours').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('No days of the week is selected', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dailyTask.repeat = {
|
||||||
|
su: false,
|
||||||
|
s: false,
|
||||||
|
f: false,
|
||||||
|
th: false,
|
||||||
|
w: false,
|
||||||
|
t: false,
|
||||||
|
m: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for a day before the Start Date', () => {
|
||||||
|
day = moment().subtract(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for the Start Date', () => {
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for a day after the Start Date', () => {
|
||||||
|
day = moment().add(1, 'days').toDate();
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for today', () => {
|
||||||
|
expect(shouldDo(day, dailyTask, options)).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// context('Every X Weeks', () => {
|
// context('Every X Weeks', () => {
|
||||||
|
|||||||
@@ -30,16 +30,17 @@ export function generateChallenge (options = {}) {
|
|||||||
|
|
||||||
export function generateRes (options = {}) {
|
export function generateRes (options = {}) {
|
||||||
let defaultRes = {
|
let defaultRes = {
|
||||||
render: sandbox.stub(),
|
|
||||||
send: sandbox.stub(),
|
|
||||||
status: sandbox.stub().returnsThis(),
|
|
||||||
sendStatus: sandbox.stub().returnsThis(),
|
|
||||||
json: sandbox.stub(),
|
json: sandbox.stub(),
|
||||||
locals: {
|
locals: {
|
||||||
user: generateUser(options.localsUser),
|
user: generateUser(options.localsUser),
|
||||||
group: generateGroup(options.localsGroup),
|
group: generateGroup(options.localsGroup),
|
||||||
},
|
},
|
||||||
|
redirect: sandbox.stub(),
|
||||||
|
render: sandbox.stub(),
|
||||||
|
send: sandbox.stub(),
|
||||||
|
sendStatus: sandbox.stub().returnsThis(),
|
||||||
set: sandbox.stub(),
|
set: sandbox.stub(),
|
||||||
|
status: sandbox.stub().returnsThis(),
|
||||||
t (string) {
|
t (string) {
|
||||||
return i18n.t(string);
|
return i18n.t(string);
|
||||||
},
|
},
|
||||||
|
|||||||
4
vagrant_scripts/install_node.sh
Executable file → Normal file
@@ -16,7 +16,7 @@ nvm use
|
|||||||
nvm alias default current
|
nvm alias default current
|
||||||
|
|
||||||
echo Update npm...
|
echo Update npm...
|
||||||
npm install -g npm@3
|
npm install -g npm@4
|
||||||
|
|
||||||
echo Installing global modules...
|
echo Installing global modules...
|
||||||
npm install -g gulp bower grunt-cli mocha
|
npm install -g gulp bower grunt-cli mocha node-pre-gyp
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
website/assets/img/project_files/npcs/spring/npc_bailey.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
website/assets/img/project_files/npcs/spring/npc_daniel.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
BIN
website/assets/img/project_files/npcs/spring/npc_justin.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
website/assets/img/project_files/npcs/spring/npc_matt.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,36 +1,36 @@
|
|||||||
.promo_android {
|
.promo_android {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -180px;
|
background-position: -1715px -176px;
|
||||||
width: 175px;
|
width: 175px;
|
||||||
height: 175px;
|
height: 175px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201602 {
|
.promo_backgrounds_armoire_201602 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -565px -600px;
|
background-position: -1573px -295px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 294px;
|
height: 294px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201603 {
|
.promo_backgrounds_armoire_201603 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -707px -600px;
|
background-position: -1573px 0px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 294px;
|
height: 294px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201604 {
|
.promo_backgrounds_armoire_201604 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -1041px;
|
background-position: -1150px -442px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201605 {
|
.promo_backgrounds_armoire_201605 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -141px -1041px;
|
background-position: -1291px -442px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201606 {
|
.promo_backgrounds_armoire_201606 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -699px 0px;
|
background-position: -699px -148px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 447px;
|
height: 447px;
|
||||||
}
|
}
|
||||||
@@ -42,49 +42,67 @@
|
|||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201608 {
|
.promo_backgrounds_armoire_201608 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -142px -600px;
|
background-position: -710px -600px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 439px;
|
height: 439px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201609 {
|
.promo_backgrounds_armoire_201609 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -283px -600px;
|
background-position: -851px -600px;
|
||||||
width: 139px;
|
width: 139px;
|
||||||
height: 438px;
|
height: 438px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201610 {
|
.promo_backgrounds_armoire_201610 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1124px -442px;
|
background-position: -1432px 0px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201611 {
|
.promo_backgrounds_armoire_201611 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1265px 0px;
|
background-position: 0px -1042px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201612 {
|
.promo_backgrounds_armoire_201612 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1265px -442px;
|
background-position: -141px -1042px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201701 {
|
.promo_backgrounds_armoire_201701 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1406px 0px;
|
background-position: -282px -1042px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201702 {
|
.promo_backgrounds_armoire_201702 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -840px 0px;
|
background-position: -284px -600px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_backgrounds_armoire_201703 {
|
.promo_backgrounds_armoire_201703 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -982px 0px;
|
background-position: -840px -148px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
|
.promo_backgrounds_armoire_201704 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -426px -600px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
|
.promo_backgrounds_armoire_201705 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: 0px -600px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
|
.promo_bees {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -142px -600px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
@@ -96,151 +114,163 @@
|
|||||||
}
|
}
|
||||||
.promo_chairs_glasses {
|
.promo_chairs_glasses {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1757px -532px;
|
background-position: -1821px -352px;
|
||||||
width: 51px;
|
width: 51px;
|
||||||
height: 210px;
|
height: 210px;
|
||||||
}
|
}
|
||||||
.promo_checkin_incentives {
|
.promo_checkin_incentives {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -423px -600px;
|
background-position: -991px -600px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 294px;
|
height: 294px;
|
||||||
}
|
}
|
||||||
.promo_classes_fall_2014 {
|
.promo_classes_fall_2014 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1207px -1337px;
|
background-position: -201px -1484px;
|
||||||
width: 321px;
|
width: 321px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
.promo_classes_fall_2015 {
|
.promo_classes_fall_2015 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -423px -895px;
|
background-position: -703px -1338px;
|
||||||
width: 377px;
|
width: 377px;
|
||||||
height: 99px;
|
height: 99px;
|
||||||
}
|
}
|
||||||
.promo_classes_fall_2016 {
|
.promo_classes_fall_2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px 0px;
|
background-position: -1573px -590px;
|
||||||
width: 103px;
|
width: 103px;
|
||||||
height: 348px;
|
height: 348px;
|
||||||
}
|
}
|
||||||
.promo_coffee_mug {
|
.promo_coffee_mug {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px 0px;
|
background-position: 0px -1484px;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 179px;
|
height: 179px;
|
||||||
}
|
}
|
||||||
.promo_contrib_spotlight_Keith {
|
.promo_contrib_spotlight_Keith {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -1118px;
|
background-position: -1715px -1536px;
|
||||||
width: 87px;
|
width: 87px;
|
||||||
height: 111px;
|
height: 111px;
|
||||||
}
|
}
|
||||||
.promo_contrib_spotlight_beffymaroo {
|
.promo_contrib_spotlight_beffymaroo {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1406px -884px;
|
background-position: -1715px -626px;
|
||||||
width: 114px;
|
width: 114px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_contrib_spotlight_blade {
|
.promo_contrib_spotlight_blade {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -1006px;
|
background-position: -1715px -1424px;
|
||||||
width: 89px;
|
width: 89px;
|
||||||
height: 111px;
|
height: 111px;
|
||||||
}
|
}
|
||||||
.promo_contrib_spotlight_cantras {
|
.promo_contrib_spotlight_cantras {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -1230px;
|
background-position: -1803px -1536px;
|
||||||
width: 87px;
|
width: 87px;
|
||||||
height: 109px;
|
height: 109px;
|
||||||
}
|
}
|
||||||
|
.promo_contrib_spotlight_dewines {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -1573px -939px;
|
||||||
|
width: 89px;
|
||||||
|
height: 108px;
|
||||||
|
}
|
||||||
.promo_contrib_spotlight_megan {
|
.promo_contrib_spotlight_megan {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -894px;
|
background-position: -1715px -1200px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 111px;
|
height: 111px;
|
||||||
}
|
}
|
||||||
.promo_contrib_spotlight_shanaqui {
|
.promo_contrib_spotlight_shanaqui {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -782px;
|
background-position: -1715px -1312px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 111px;
|
height: 111px;
|
||||||
}
|
}
|
||||||
.promo_cow {
|
.promo_cow {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -282px -1041px;
|
background-position: -1432px -442px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_cupid_potions {
|
.promo_cupid_potions {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -705px -1041px;
|
background-position: -564px -1042px;
|
||||||
width: 138px;
|
width: 138px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_dilatoryDistress {
|
.promo_dilatoryDistress {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1286px -1644px;
|
background-position: -1474px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_egg_mounts {
|
.promo_egg_mounts {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -844px -1041px;
|
background-position: -703px -1190px;
|
||||||
width: 280px;
|
width: 280px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire {
|
.promo_enchanted_armoire {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -1483px;
|
background-position: -1081px -1338px;
|
||||||
width: 374px;
|
width: 374px;
|
||||||
height: 76px;
|
height: 76px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201507 {
|
.promo_enchanted_armoire_201507 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -507px -1644px;
|
background-position: -1341px -1042px;
|
||||||
width: 217px;
|
width: 217px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201508 {
|
.promo_enchanted_armoire_201508 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -1342px;
|
background-position: -468px -1664px;
|
||||||
width: 180px;
|
width: 180px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201509 {
|
.promo_enchanted_armoire_201509 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -614px -1735px;
|
background-position: -928px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201511 {
|
.promo_enchanted_armoire_201511 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -982px -442px;
|
background-position: -1715px -1018px;
|
||||||
width: 122px;
|
width: 122px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_enchanted_armoire_201601 {
|
.promo_enchanted_armoire_201601 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -887px -1735px;
|
background-position: -364px -1767px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
|
.promo_fairy_potions {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -982px -148px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
.promo_floral_potions {
|
.promo_floral_potions {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -532px;
|
background-position: -1715px -352px;
|
||||||
width: 105px;
|
width: 105px;
|
||||||
height: 273px;
|
height: 273px;
|
||||||
}
|
}
|
||||||
.promo_ghost_potions {
|
.promo_ghost_potions {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1406px -442px;
|
background-position: -1291px 0px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_habitica {
|
.promo_habitica {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -356px;
|
background-position: -1715px 0px;
|
||||||
width: 175px;
|
width: 175px;
|
||||||
height: 175px;
|
height: 175px;
|
||||||
}
|
}
|
||||||
@@ -252,217 +282,223 @@
|
|||||||
}
|
}
|
||||||
.promo_habitoween_2016 {
|
.promo_habitoween_2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -423px -1041px;
|
background-position: -1150px 0px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_haunted_hair {
|
.promo_haunted_hair {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1547px -644px;
|
background-position: -1715px -774px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 137px;
|
height: 137px;
|
||||||
}
|
}
|
||||||
.promo_holly_potions {
|
.promo_holly_potions {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -600px;
|
background-position: -568px -600px;
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 440px;
|
height: 440px;
|
||||||
}
|
}
|
||||||
.promo_item_notif {
|
.promo_item_notif {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -1735px;
|
background-position: 0px -1664px;
|
||||||
width: 249px;
|
width: 249px;
|
||||||
height: 102px;
|
height: 102px;
|
||||||
}
|
}
|
||||||
.promo_jackalope {
|
.promo_jackalope {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1124px -1189px;
|
background-position: -1264px -1190px;
|
||||||
width: 276px;
|
width: 276px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
|
.promo_more_checkin_incentives {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -699px 0px;
|
||||||
|
width: 450px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
.promo_mystery_201405 {
|
.promo_mystery_201405 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1650px -1644px;
|
background-position: 0px -1767px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201406 {
|
.promo_mystery_201406 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1401px -1189px;
|
background-position: -1432px -884px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201407 {
|
.promo_mystery_201407 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1809px -669px;
|
background-position: -1821px -563px;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 62px;
|
height: 62px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201408 {
|
.promo_mystery_201408 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1051px -812px;
|
background-position: -1821px -912px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 71px;
|
height: 71px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201409 {
|
.promo_mystery_201409 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1195px -1644px;
|
background-position: -182px -1767px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201410 {
|
.promo_mystery_201410 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -982px -533px;
|
background-position: -1806px -1200px;
|
||||||
width: 72px;
|
width: 72px;
|
||||||
height: 63px;
|
height: 63px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201411 {
|
.promo_mystery_201411 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1468px -1644px;
|
background-position: -837px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201412 {
|
.promo_mystery_201412 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1809px -602px;
|
background-position: -1836px -1109px;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 66px;
|
height: 66px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201501 {
|
.promo_mystery_201501 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1792px -806px;
|
background-position: -1830px -708px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 63px;
|
height: 63px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201502 {
|
.promo_mystery_201502 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -341px -1735px;
|
background-position: -91px -1767px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201503 {
|
.promo_mystery_201503 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -432px -1735px;
|
background-position: -1656px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201504 {
|
.promo_mystery_201504 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -375px -1483px;
|
background-position: -1806px -1312px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 69px;
|
height: 69px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201505 {
|
.promo_mystery_201505 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -796px -1735px;
|
background-position: -1110px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201506 {
|
.promo_mystery_201506 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1809px -532px;
|
background-position: -1838px -1018px;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 69px;
|
height: 69px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201507 {
|
.promo_mystery_201507 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -990px -600px;
|
background-position: -1573px -1260px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201508 {
|
.promo_mystery_201508 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -819px -1644px;
|
background-position: -1291px -884px;
|
||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201509 {
|
.promo_mystery_201509 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1377px -1644px;
|
background-position: -273px -1767px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201510 {
|
.promo_mystery_201510 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -725px -1644px;
|
background-position: -1150px -884px;
|
||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201511 {
|
.promo_mystery_201511 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1559px -1644px;
|
background-position: -1019px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201512 {
|
.promo_mystery_201512 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -990px -812px;
|
background-position: -1830px -626px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 81px;
|
height: 81px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201601 {
|
.promo_mystery_201601 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -840px -442px;
|
background-position: -1715px -1109px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201602 {
|
.promo_mystery_201602 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -250px -1735px;
|
background-position: -1292px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201603 {
|
.promo_mystery_201603 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1741px -1644px;
|
background-position: -1383px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201604 {
|
.promo_mystery_201604 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1101px -1644px;
|
background-position: -991px -895px;
|
||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201605 {
|
.promo_mystery_201605 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -523px -1735px;
|
background-position: -1565px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201606 {
|
.promo_mystery_201606 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -699px -448px;
|
background-position: -1573px -1048px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201607 {
|
.promo_mystery_201607 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -705px -1735px;
|
background-position: -1747px -1664px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201608 {
|
.promo_mystery_201608 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -913px -1644px;
|
background-position: -649px -1664px;
|
||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201609 {
|
.promo_mystery_201609 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1007px -1644px;
|
background-position: -743px -1664px;
|
||||||
width: 93px;
|
width: 93px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201610 {
|
.promo_mystery_201610 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1771px -1194px;
|
background-position: -1816px -774px;
|
||||||
width: 63px;
|
width: 63px;
|
||||||
height: 84px;
|
height: 84px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201611 {
|
.promo_mystery_201611 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1405px -1041px;
|
background-position: -1573px -1366px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 99px;
|
height: 99px;
|
||||||
}
|
}
|
||||||
@@ -474,151 +510,73 @@
|
|||||||
}
|
}
|
||||||
.promo_mystery_201701 {
|
.promo_mystery_201701 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -990px -706px;
|
background-position: -1573px -1154px;
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201702 {
|
.promo_mystery_201702 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1125px -1041px;
|
background-position: -984px -1190px;
|
||||||
width: 279px;
|
width: 279px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
|
.promo_mystery_201703 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -1058px -1042px;
|
||||||
|
width: 282px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_mystery_201704 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
|
background-position: -1201px -1664px;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
.promo_mystery_3014 {
|
.promo_mystery_3014 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -289px -1644px;
|
background-position: -250px -1664px;
|
||||||
width: 217px;
|
width: 217px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_new_hair_fall2016 {
|
.promo_new_hair_fall2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -564px -1041px;
|
background-position: -423px -1042px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_orca {
|
.promo_orca {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1124px -884px;
|
background-position: -1715px -912px;
|
||||||
width: 105px;
|
width: 105px;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
}
|
}
|
||||||
.promo_partyhats {
|
.promo_partyhats {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -1433px;
|
background-position: -1432px -981px;
|
||||||
width: 115px;
|
width: 115px;
|
||||||
height: 47px;
|
height: 47px;
|
||||||
}
|
}
|
||||||
.promo_pastel_skin {
|
.promo_pastel_skin {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: 0px -1560px;
|
background-position: -523px -1484px;
|
||||||
width: 330px;
|
width: 330px;
|
||||||
height: 83px;
|
height: 83px;
|
||||||
}
|
}
|
||||||
.customize-option.promo_pastel_skin {
|
.customize-option.promo_pastel_skin {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -25px -1575px;
|
background-position: -548px -1499px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.promo_peppermint_flame {
|
.promo_pastel_skin_hair {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -954px;
|
background-position: -703px -1042px;
|
||||||
width: 140px;
|
width: 354px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_pet_skins {
|
.customize-option.promo_pastel_skin_hair {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||||
background-position: -1651px -806px;
|
background-position: -728px -1057px;
|
||||||
width: 140px;
|
|
||||||
height: 147px;
|
|
||||||
}
|
|
||||||
.customize-option.promo_pet_skins {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1676px -821px;
|
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.promo_pyromancer {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1265px -884px;
|
|
||||||
width: 113px;
|
|
||||||
height: 113px;
|
|
||||||
}
|
|
||||||
.promo_rainbow_armor {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1547px -1340px;
|
|
||||||
width: 92px;
|
|
||||||
height: 103px;
|
|
||||||
}
|
|
||||||
.promo_seasonal_shop_fall_2016 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -844px -1189px;
|
|
||||||
width: 279px;
|
|
||||||
height: 147px;
|
|
||||||
}
|
|
||||||
.promo_shimmer_hair {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -331px -1560px;
|
|
||||||
width: 330px;
|
|
||||||
height: 83px;
|
|
||||||
}
|
|
||||||
.promo_splashyskins {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1651px -1102px;
|
|
||||||
width: 198px;
|
|
||||||
height: 91px;
|
|
||||||
}
|
|
||||||
.customize-option.promo_splashyskins {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1676px -1117px;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
.promo_spooky_sparkles_fall_2016 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -849px -600px;
|
|
||||||
width: 140px;
|
|
||||||
height: 294px;
|
|
||||||
}
|
|
||||||
.promo_spring_classes_2016 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -844px -1337px;
|
|
||||||
width: 362px;
|
|
||||||
height: 102px;
|
|
||||||
}
|
|
||||||
.promo_springclasses2014 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -801px -895px;
|
|
||||||
width: 288px;
|
|
||||||
height: 90px;
|
|
||||||
}
|
|
||||||
.promo_springclasses2015 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: 0px -1644px;
|
|
||||||
width: 288px;
|
|
||||||
height: 90px;
|
|
||||||
}
|
|
||||||
.promo_staff_spotlight_Lemoness {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1547px -349px;
|
|
||||||
width: 102px;
|
|
||||||
height: 146px;
|
|
||||||
}
|
|
||||||
.promo_staff_spotlight_Viirus {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1651px -1194px;
|
|
||||||
width: 119px;
|
|
||||||
height: 147px;
|
|
||||||
}
|
|
||||||
.promo_staff_spotlight_paglias {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1547px -496px;
|
|
||||||
width: 99px;
|
|
||||||
height: 147px;
|
|
||||||
}
|
|
||||||
.promo_steampunk_3017 {
|
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
|
||||||
background-position: -1124px 0px;
|
|
||||||
width: 140px;
|
|
||||||
height: 441px;
|
|
||||||
}
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
@@ -1,198 +1,330 @@
|
|||||||
|
.promo_peppermint_flame {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1629px -934px;
|
||||||
|
width: 140px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_pet_skins {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1488px -934px;
|
||||||
|
width: 140px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.customize-option.promo_pet_skins {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1513px -949px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.promo_pyromancer {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1639px -632px;
|
||||||
|
width: 113px;
|
||||||
|
height: 113px;
|
||||||
|
}
|
||||||
|
.promo_rainbow_armor {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1358px -556px;
|
||||||
|
width: 92px;
|
||||||
|
height: 103px;
|
||||||
|
}
|
||||||
|
.promo_seasonal_shop_fall_2016 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -301px -1207px;
|
||||||
|
width: 279px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_shimmer_hair {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -581px -1207px;
|
||||||
|
width: 330px;
|
||||||
|
height: 83px;
|
||||||
|
}
|
||||||
|
.promo_shimmer_potions {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -934px 0px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
|
.promo_shinySeeds {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -792px 0px;
|
||||||
|
width: 141px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
|
.promo_splashyskins {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1488px -1230px;
|
||||||
|
width: 198px;
|
||||||
|
height: 91px;
|
||||||
|
}
|
||||||
|
.customize-option.promo_splashyskins {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1513px -1245px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.promo_spooky_sparkles_fall_2016 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1217px -377px;
|
||||||
|
width: 140px;
|
||||||
|
height: 294px;
|
||||||
|
}
|
||||||
|
.promo_spring_classes_2016 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -282px -1091px;
|
||||||
|
width: 362px;
|
||||||
|
height: 102px;
|
||||||
|
}
|
||||||
|
.promo_spring_classes_2017 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -899px -916px;
|
||||||
|
width: 309px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_springclasses2014 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1488px -91px;
|
||||||
|
width: 288px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
.promo_springclasses2015 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1488px 0px;
|
||||||
|
width: 288px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
.promo_staff_spotlight_Lemoness {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1639px -330px;
|
||||||
|
width: 102px;
|
||||||
|
height: 146px;
|
||||||
|
}
|
||||||
|
.promo_staff_spotlight_Viirus {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1645px -182px;
|
||||||
|
width: 119px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_staff_spotlight_paglias {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -1639px -481px;
|
||||||
|
width: 99px;
|
||||||
|
height: 147px;
|
||||||
|
}
|
||||||
|
.promo_steampunk_3017 {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: -141px -765px;
|
||||||
|
width: 140px;
|
||||||
|
height: 441px;
|
||||||
|
}
|
||||||
.promo_summer_classes_2014 {
|
.promo_summer_classes_2014 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px -591px;
|
background-position: -340px -220px;
|
||||||
width: 429px;
|
width: 429px;
|
||||||
height: 102px;
|
height: 102px;
|
||||||
}
|
}
|
||||||
.promo_summer_classes_2015 {
|
.promo_summer_classes_2015 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -553px -615px;
|
background-position: 0px -1358px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 88px;
|
height: 88px;
|
||||||
}
|
}
|
||||||
.promo_summer_classes_2016 {
|
.promo_summer_classes_2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -452px -145px;
|
background-position: -282px -765px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
.promo_takeThis_gear {
|
.promo_takeThis_gear {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -361px -882px;
|
background-position: -1639px -783px;
|
||||||
width: 114px;
|
width: 114px;
|
||||||
height: 87px;
|
height: 87px;
|
||||||
}
|
}
|
||||||
.promo_takethis_armor {
|
.promo_takethis_armor {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -476px -882px;
|
background-position: -1358px -377px;
|
||||||
width: 114px;
|
width: 114px;
|
||||||
height: 87px;
|
height: 87px;
|
||||||
}
|
}
|
||||||
.promo_task_planning {
|
.promo_task_planning {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px -96px;
|
background-position: -1217px -181px;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 195px;
|
height: 195px;
|
||||||
}
|
}
|
||||||
.promo_turkey_day_2016 {
|
.promo_turkey_day_2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -141px -440px;
|
background-position: -1076px 0px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.promo_unconventional_armor {
|
.promo_unconventional_armor {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1315px -591px;
|
background-position: -1639px -871px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.promo_unconventional_armor2 {
|
.promo_unconventional_armor2 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1283px -694px;
|
background-position: -1687px -1230px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
height: 74px;
|
height: 74px;
|
||||||
}
|
}
|
||||||
.promo_updos {
|
.promo_updos {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1195px -292px;
|
background-position: -1488px -182px;
|
||||||
width: 156px;
|
width: 156px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_veteran_pets {
|
.promo_veteran_pets {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -706px -704px;
|
background-position: -1627px -1082px;
|
||||||
width: 146px;
|
width: 146px;
|
||||||
height: 75px;
|
height: 75px;
|
||||||
}
|
}
|
||||||
.promo_winter_classes_2016 {
|
.promo_winter_classes_2016 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: 0px -882px;
|
background-position: -645px -1091px;
|
||||||
width: 360px;
|
width: 360px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.promo_winter_classes_2017 {
|
.promo_winter_classes_2017 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -452px 0px;
|
background-position: 0px -620px;
|
||||||
width: 432px;
|
width: 432px;
|
||||||
height: 144px;
|
height: 144px;
|
||||||
}
|
}
|
||||||
.promo_winter_fireworks {
|
.promo_winter_fireworks {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -302px -973px;
|
background-position: -1488px -1082px;
|
||||||
width: 138px;
|
width: 138px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_winterclasses2015 {
|
.promo_winterclasses2015 {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -452px -296px;
|
background-position: -433px -620px;
|
||||||
width: 325px;
|
width: 325px;
|
||||||
height: 110px;
|
height: 110px;
|
||||||
}
|
}
|
||||||
.promo_wintery_skins {
|
.promo_wintery_skins {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: 0px -440px;
|
background-position: 0px -765px;
|
||||||
width: 140px;
|
width: 140px;
|
||||||
height: 441px;
|
height: 441px;
|
||||||
}
|
}
|
||||||
.customize-option.promo_wintery_skins {
|
.customize-option.promo_wintery_skins {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -25px -455px;
|
background-position: -25px -780px;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
}
|
}
|
||||||
.promo_winteryhair {
|
.promo_winteryhair {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -553px -704px;
|
background-position: -1488px -1322px;
|
||||||
width: 152px;
|
width: 152px;
|
||||||
height: 75px;
|
height: 75px;
|
||||||
}
|
}
|
||||||
.avatar_variety {
|
.avatar_variety {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px 0px;
|
background-position: -683px -765px;
|
||||||
width: 498px;
|
width: 498px;
|
||||||
height: 95px;
|
height: 95px;
|
||||||
}
|
}
|
||||||
.npc_viirus {
|
.npc_viirus {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -441px -973px;
|
background-position: -1358px -465px;
|
||||||
width: 108px;
|
width: 108px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
}
|
}
|
||||||
.party_preview {
|
.party_preview {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: 0px 0px;
|
background-position: -340px 0px;
|
||||||
width: 451px;
|
width: 451px;
|
||||||
height: 219px;
|
height: 219px;
|
||||||
}
|
}
|
||||||
.promo_backtoschool {
|
.promo_backtoschool {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1186px -440px;
|
background-position: -1488px -632px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
.promo_cooking {
|
.promo_cooking {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: 0px -220px;
|
background-position: -328px -343px;
|
||||||
width: 396px;
|
width: 396px;
|
||||||
height: 219px;
|
height: 219px;
|
||||||
}
|
}
|
||||||
.promo_startingover {
|
.promo_startingover {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1132px -694px;
|
background-position: -1488px -783px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
.promo_valentines {
|
.promo_valentines {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px -292px;
|
background-position: -589px -916px;
|
||||||
width: 309px;
|
width: 309px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_working_out {
|
.promo_working_out {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px -440px;
|
background-position: 0px -1207px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
.scene_coding {
|
.scene_coding {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -151px -973px;
|
background-position: -1488px -481px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
.scene_dailies {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: 0px -343px;
|
||||||
|
width: 327px;
|
||||||
|
height: 276px;
|
||||||
|
}
|
||||||
.scene_eco_friendly {
|
.scene_eco_friendly {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -589px -440px;
|
background-position: -1217px -1004px;
|
||||||
width: 222px;
|
width: 222px;
|
||||||
height: 171px;
|
height: 171px;
|
||||||
}
|
}
|
||||||
.scene_habits {
|
.scene_habits {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -282px -440px;
|
background-position: -282px -916px;
|
||||||
width: 306px;
|
width: 306px;
|
||||||
height: 174px;
|
height: 174px;
|
||||||
}
|
}
|
||||||
.scene_phone_peek {
|
.scene_phone_peek {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: 0px -973px;
|
background-position: -1488px -330px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
.scene_video_games {
|
||||||
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
|
background-position: 0px 0px;
|
||||||
|
width: 339px;
|
||||||
|
height: 342px;
|
||||||
|
}
|
||||||
.welcome_basic_avatars {
|
.welcome_basic_avatars {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -885px -694px;
|
background-position: -1217px -838px;
|
||||||
width: 246px;
|
width: 246px;
|
||||||
height: 165px;
|
height: 165px;
|
||||||
}
|
}
|
||||||
.welcome_promo_party {
|
.welcome_promo_party {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -282px -615px;
|
background-position: -1217px 0px;
|
||||||
width: 270px;
|
width: 270px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
.welcome_sample_tasks {
|
.welcome_sample_tasks {
|
||||||
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
|
||||||
background-position: -1126px -96px;
|
background-position: -1217px -672px;
|
||||||
width: 246px;
|
width: 246px;
|
||||||
height: 165px;
|
height: 165px;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 340 KiB |
1598
website/assets/sprites/dist/spritesmith-main-0.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-0.png
vendored
|
Before Width: | Height: | Size: 478 KiB After Width: | Height: | Size: 507 KiB |
3250
website/assets/sprites/dist/spritesmith-main-1.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-1.png
vendored
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 72 KiB |
1570
website/assets/sprites/dist/spritesmith-main-10.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-10.png
vendored
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 141 KiB |
1908
website/assets/sprites/dist/spritesmith-main-11.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-11.png
vendored
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 138 KiB |
1886
website/assets/sprites/dist/spritesmith-main-12.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-12.png
vendored
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 158 KiB |
2492
website/assets/sprites/dist/spritesmith-main-13.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-13.png
vendored
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 146 KiB |
2756
website/assets/sprites/dist/spritesmith-main-14.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-14.png
vendored
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 154 KiB |
3022
website/assets/sprites/dist/spritesmith-main-15.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-15.png
vendored
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 177 KiB |
2568
website/assets/sprites/dist/spritesmith-main-16.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-16.png
vendored
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 159 KiB |
1554
website/assets/sprites/dist/spritesmith-main-17.css
vendored
Normal file
BIN
website/assets/sprites/dist/spritesmith-main-17.png
vendored
Normal file
|
After Width: | Height: | Size: 122 KiB |
6846
website/assets/sprites/dist/spritesmith-main-2.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-2.png
vendored
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 67 KiB |
6130
website/assets/sprites/dist/spritesmith-main-3.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-3.png
vendored
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
3370
website/assets/sprites/dist/spritesmith-main-4.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-4.png
vendored
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 110 KiB |
5580
website/assets/sprites/dist/spritesmith-main-5.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-5.png
vendored
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 153 KiB |
2150
website/assets/sprites/dist/spritesmith-main-6.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-6.png
vendored
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
1694
website/assets/sprites/dist/spritesmith-main-7.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-7.png
vendored
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 195 KiB |
1850
website/assets/sprites/dist/spritesmith-main-8.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-8.png
vendored
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 440 KiB |