Compare commits

..

1 Commits

Author SHA1 Message Date
Phillip Thelen
8a317c269e Fix news related permission issues 2024-08-01 15:24:19 +02:00
1227 changed files with 34504 additions and 38928 deletions

View File

@@ -9,4 +9,4 @@
}
]
]
}
}

View File

@@ -7,14 +7,5 @@ module.exports = {
rules: {
'prefer-regex-literals': 'warn',
'import/no-extraneous-dependencies': 'off',
'require-await': 'error',
},
overrides: [
{
files: ['migrations/**', 'gulp/**'], // Or *.test.js
rules: {
'require-await': 'off',
},
},
],
};

View File

@@ -1,13 +1,6 @@
name: Test
on:
push:
branches-ignore:
- 'phillip/**'
- 'sabrecat/**'
- 'kalista/**'
- 'natalie/**'
pull_request:
on: [push, pull_request]
permissions:
contents: read
@@ -26,8 +19,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -49,8 +41,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -72,8 +63,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -96,8 +86,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -119,8 +108,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -149,8 +137,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -180,8 +167,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -211,8 +197,7 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -237,8 +222,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -262,8 +246,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |

2
.gitignore vendored
View File

@@ -47,5 +47,5 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data*
/mongodb-data
/.nyc_output

View File

@@ -1,25 +0,0 @@
#!/bin/bash
DEVELOPER="someone"
if git rev-parse --git-dir > /dev/null 2>&1; then
DEVELOPERS=$(git log -5 --pretty=format:'%an')
IFS=$'\n'
DEVELOPER=""
for dev in $DEVELOPERS
do
if [ "$DEVELOPER" == "someone" ]; then
if [[ ${dev} != *"[bot]"* ]]; then
DEVELOPER=$dev
continue
fi
continue
fi
done
fi
PARTS=$(cut -d"." -f1 <<< $BASE_URL)
SERVER_NAME=$(cut -d"/" -f3 <<< ${PARTS[0]})
SERVER_NAME=":$SERVER_EMOJI: $SERVER_NAME"
wget $SLACK_DEPLOY_URL --post-data="{\"server_name\": \"$SERVER_NAME\", \"developer\": \"$DEVELOPER\", \"base_url\": \"$BASE_URL\"}" -O /dev/null

View File

@@ -2,4 +2,4 @@
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.

View File

@@ -37,7 +37,6 @@
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
@@ -93,6 +92,5 @@
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
"SLOW_REQUEST_THRESHOLD": 1000
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
}

View File

@@ -22,8 +22,7 @@ services:
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
- mongo
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
networks:
@@ -34,16 +33,7 @@ services:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: mongo:5.0.23
restart: unless-stopped
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
image: mongo:3.6
networks:
- habitica
ports:

View File

@@ -64,15 +64,6 @@ function filterFile (file) {
if (file.relative.indexOf('icon_background') === 0) {
return false;
}
if (file.relative.indexOf('notif_') === 0) {
return false;
}
if (file.relative.indexOf('quest_') === 0) {
return false;
}
if (file.relative.indexOf('inventory_quest_') === 0) {
return false;
}
return true;
}

11
gulp/gulp-start.js Normal file
View File

@@ -0,0 +1,11 @@
import gulp from 'gulp';
import nodemon from 'gulp-nodemon';
import pkg from '../package.json';
gulp.task('nodemon', done => {
nodemon({
script: pkg.main,
});
done();
});

View File

@@ -49,6 +49,12 @@ function integrationTestCommand (testDir) {
}
/* Test task definitions */
gulp.task('test:nodemon', gulp.series(done => {
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
done();
}, 'nodemon'));
gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);

View File

@@ -21,6 +21,7 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
require('./gulp/gulp-build'); // eslint-disable-line global-require
require('./gulp/gulp-console'); // eslint-disable-line global-require
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
require('./gulp/gulp-start'); // eslint-disable-line global-require
require('./gulp/gulp-tests'); // eslint-disable-line global-require
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require

View File

@@ -1,47 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '2024_purge_invite_accepted';
const progressCount = 1000;
let count = 0;
async function updateUsers (userIds) {
count += userIds.length;
if (count % progressCount === 0) console.warn(`${count} ${userIds[0]}`);
return await User.updateMany(
{ _id: { $in: userIds } },
{ $pull: { notifications: { type: 'GROUP_INVITE_ACCEPTED' } } },
).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'notifications.type': 'GROUP_INVITE_ACCEPTED',
'auth.timestamps.loggedin': { $gt: new Date('2024-06-25') },
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.select({ _id: 1 })
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
const userIds = users.map(user => user._id);
await updateUsers(userIds); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,115 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20241119_gem_caps_hourglasses';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
const { consecutive, customerId, dateTerminated, planId } = user.purchased.plan;
const isRecurring = customerId !== 'Gift' && !dateTerminated;
const updateOp = {
$set: {
migration: MIGRATION_NAME,
'purchased.plan.consecutive.gemCapExtra': Math.max(2 * Math.ceil((consecutive.gemCapExtra + 1) / 2, 26)),
},
$inc: {},
};
let hourglassBonus = 0;
if (isRecurring) {
await user.updateBalance(
5,
'admin_update_balance',
'',
'Subscription Reward Migration',
);
updateOp.$inc.balance = 5;
switch (planId) {
case 'basic':
case 'basic_earned':
case 'group_plan_auto':
hourglassBonus = 2;
break;
case 'basic_3mo':
case 'basic_6mo':
case 'google_6mo':
hourglassBonus = 4;
break;
case 'basic_12mo':
hourglassBonus = 12;
updateOp.$set['purchased.plan.hourglassPromoReceived'] = new Date();
break;
default:
hourglassBonus = 0;
}
if (hourglassBonus) {
updateOp.$inc['purchased.plan.consecutive.trinkets'] = hourglassBonus;
await user.updateHourglasses(
hourglassBonus,
'admin_update_balance',
'',
'Subscription Reward Migration',
);
}
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_subscriber_reward',
title: 'Thanks for being a subscriber!',
text: 'Enjoy these extra Mystic Hourglasses and Gems to celebrate our new benefits.',
},
seen: false,
},
};
}
return await User.updateOne(
{ _id: user._id },
updateOp,
).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
};
const fields = {
_id: 1,
purchased: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -37,7 +37,7 @@ let consoleStamp = require('console-stamp');
consoleStamp(console);
// Initialize configuration
require('../../website/server/libs/api-v3/setupNconf').default();
require('../../website/server/libs/api-v3/setupNconf')();
let MONGODB_OLD = nconf.get('MONGODB_OLD');
let MONGODB_NEW = nconf.get('MONGODB_NEW');

View File

@@ -32,7 +32,7 @@ let moment = require('moment');
consoleStamp(console);
// Initialize configuration
require('../../website/server/libs/api-v3/setupNconf').default();
require('../../website/server/libs/api-v3/setupNconf')();
let MONGODB_OLD = nconf.get('MONGODB_OLD');
let MONGODB_NEW = nconf.get('MONGODB_NEW');

View File

@@ -6,7 +6,7 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
function setUpServer () {
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
const setupNconf = require('../website/server/libs/setupNconf').default; // eslint-disable-line global-require
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
setupNconf();

View File

@@ -26,7 +26,7 @@ async function updateUser (user) {
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
);
return User.updateOne({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
return User.update({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
}
export default async function processUsers () {

View File

@@ -27,13 +27,13 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.updateOne({ _id: user._id }, { $set: set }).exec();
return User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.local.username': 'ExampleHabitican',
'auth.local.lowerCaseUsername': 'olson1',
};
const fields = {

View File

@@ -57,7 +57,7 @@ async function updateUser (user) {
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.local.username': 'ExampleHabitican',
'auth.local.username': 'SabreTest',
};
const fields = {

View File

@@ -1,175 +0,0 @@
/*
* Award Habitoween ladder items to participants in this month's Habitoween festivities
*/
/* eslint-disable no-console */
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20241030_habitoween_ladder'; // Update when running in future years
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const set = { migration: MIGRATION_NAME };
const inc = {
'items.food.Candy_Skeleton': 1,
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Zombie': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Red': 1,
};
const push = { notifications: { $each: [] } };
if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-RoyalPurple']) {
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_candy',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-RoyalPurple']) {
set['items.mounts.JackOLantern-RoyalPurple'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_purple_mount',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Mount and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
set['items.pets.JackOLantern-RoyalPurple'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_purple_pet',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Pet and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
set['items.mounts.JackOLantern-Glow'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_glow_mount',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Mount and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
set['items.pets.JackOLantern-Glow'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_glow_pet',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Pet and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
set['items.mounts.JackOLantern-Ghost'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_ghost_mount',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Mount and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
set['items.pets.JackOLantern-Ghost'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_ghost_pet',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Pet and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
set['items.mounts.JackOLantern-Base'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_base_mount',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Mount and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.JackOLantern-Base'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_habitoween_base_pet',
title: 'Happy Habitoween!',
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Pet and an assortment of candy for your Pets!',
destination: '/inventory/stable',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.updateOne({ _id: user._id }, { $inc: inc, $push: push, $set: set }).exec();
}
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-10-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
}

View File

@@ -1,167 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20241120_harvest_feast';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const updateOp = {
$set: { migration: MIGRATION_NAME },
};
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
updateOp.$inc = {
'items.food.Pie_Base': 1,
'items.food.Pie_CottonCandyBlue': 1,
'items.food.Pie_CottonCandyPink': 1,
'items.food.Pie_Desert': 1,
'items.food.Pie_Golden': 1,
'items.food.Pie_Red': 1,
'items.food.Pie_Shade': 1,
'items.food.Pie_Skeleton': 1,
'items.food.Pie_Zombie': 1,
'items.food.Pie_White': 1,
};
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_pie',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received an assortment of pie for your Pets!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_turkeyHelmGilded'] = true;
updateOp.$set['items.gear.owned.armor_special_turkeyArmorGilded'] = true;
updateOp.$set['items.gear.owned.back_special_turkeyTailGilded'] = true;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_gilded_set',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Gilded Turkey Armor, Helm, and Tail!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
updateOp.$set['items.gear.owned.head_special_turkeyHelmBase'] = true;
updateOp.$set['items.gear.owned.armor_special_turkeyArmorBase'] = true;
updateOp.$set['items.gear.owned.back_special_turkeyTailBase'] = true;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_base_set',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Turkey Armor, Helm, and Tail!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
updateOp.$set['items.mounts.Turkey-Gilded'] = true;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_gilded_mount',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Gilded Turkey Mount!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
updateOp.$set['items.pets.Turkey-Gilded'] = 5;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_gilded_pet',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Gilded Turkey Pet!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
updateOp.$set['items.mounts.Turkey-Base'] = true;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_base_mount',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Turkey Mount!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else {
updateOp.$set['items.pets.Turkey-Base'] = 5;
updateOp.$push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_harvestfeast_base_pet',
title: 'Happy Harvest Feast!',
text: 'Gobble gobble, you\'ve received the Turkey Pet!',
destination: '/inventory/stable',
},
seen: false,
},
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.updateOne({ _id: user._id }, updateOp).exec();
}
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-10-20') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
}

View File

@@ -1,125 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20231228_nye';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const updateOp = {
$set: { migration: MIGRATION_NAME },
$push: { },
};
const data = {
title: 'Happy New Year!',
destination: '/inventory/equipment',
};
if (typeof user.items.gear.owned.head_special_nye2023 !== 'undefined') {
updateOp.$inc = {
'items.food.Candy_Skeleton': 1,
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Zombie': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Red': 1,
};
data.icon = 'notif_candy_nye';
data.text = 'Youve received an assortment of candy to celebrate with your Pets!';
data.destination = '/inventory/stable';
} else if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2023'] = true;
data.icon = 'notif_2023hat_nye';
data.text = 'Take on your resolutions with style in this Ludicrous Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2022'] = true;
data.icon = 'notif_2022hat_nye';
data.text = 'Take on your resolutions with style in this Fabulous Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2021'] = true;
data.icon = 'notif_2021hat_nye';
data.text = 'Take on your resolutions with style in this Preposterous Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2020'] = true;
data.icon = 'notif_2020hat_nye';
data.text = 'Take on your resolutions with style in this Extravagant Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2019'] = true;
data.icon = 'notif_2019hat_nye';
data.text = 'Take on your resolutions with style in this Outrageous Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2018'] = true;
data.icon = 'notif_2018hat_nye';
data.text = 'Take on your resolutions with style in this Outlandish Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2017'] = true;
data.icon = 'notif_2017hat_nye';
data.text = 'Take on your resolutions with style in this Fanciful Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2016'] = true;
data.icon = 'notif_2016hat_nye';
data.text = 'Take on your resolutions with style in this Whimsical Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2015'] = true;
data.icon = 'notif_2015hat_nye';
data.text = 'Take on your resolutions with style in this Ridiculous Party Hat!';
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
updateOp.$set['items.gear.owned.head_special_nye2014'] = true;
data.icon = 'notif_2014hat_nye';
data.text = 'Take on your resolutions with style in this Silly Party Hat!';
} else {
updateOp.$set['items.gear.owned.head_special_nye'] = true;
data.icon = 'notif_2013hat_nye';
data.text = 'Take on your resolutions with style in this Absurd Party Hat!';
}
updateOp.$push.notifications = {
type: 'ITEM_RECEIVED',
data,
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.updateOne({ _id: user._id }, updateOp).exec();
}
export default async function processUsers () {
const query = {
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
migration: { $ne: MIGRATION_NAME },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
}

1896
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.38.1",
"version": "5.27.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -15,9 +15,8 @@
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"babel-preset-env": "^1.7.0",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.2",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
@@ -28,7 +27,7 @@
"eslint": "^8.55.0",
"eslint-config-habitrpg": "^6.2.3",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.21.1",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
@@ -38,6 +37,7 @@
"gulp-babel": "^8.0.0",
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
@@ -49,11 +49,12 @@
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"moment-recur": "^1.0.7",
"mongoose": "^7.6.3",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"nodemon": "^2.0.20",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
@@ -74,7 +75,6 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
@@ -98,27 +98,26 @@
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
"test:nodemon": "gulp test:nodemon",
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:build": "cd website/client && npm run build",
"client:unit": "cd website/client && npm run test:unit",
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "node --watch --inspect ./website/server/index.js",
"start": "gulp nodemon",
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
"heroku-postbuild": "npm run client:build"
},
"devDependencies": {
"axios": "^1.8.2",
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
"chalk": "^5.3.0",
"cross-spawn": "^7.0.5",
"cross-spawn": "^7.0.3",
"mocha": "^5.1.1",
"monk": "^7.3.4",
"nyc": "^15.1.0",

View File

@@ -71,14 +71,15 @@ async function deleteHabiticaData (user, email) {
}
async function processEmailAddress (email) {
const emailRegex = new RegExp(`^${email}$`, 'i');
const localUsers = await User.find(
{ 'auth.local.email': email },
{ 'auth.local.email': emailRegex },
{ _id: 1, apiToken: 1, auth: 1 },
).exec();
const socialUsers = await User.find(
{
'auth.local.email': { $ne: email },
'auth.local.email': { $not: emailRegex },
$or: [
{ 'auth.facebook.emails.value': email },
{ 'auth.google.emails.value': email },

View File

@@ -8,17 +8,7 @@ const TASK_VALUE_CHANGE_FACTOR = 0.9747;
const MIN_TASK_VALUE = -47.27;
async function updateTeamTasks (team) {
if (team.purchased.plan.dateTerminated) {
const dateTerminated = new Date(team.purchased.plan.dateTerminated);
if (dateTerminated < new Date()) {
team.purchased.plan.customerId = undefined;
team.markModified('purchased.plan');
return team.save();
}
}
const toSave = [];
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
if (!teamLeader) { // why would this happen?
@@ -103,7 +93,12 @@ async function updateTeamTasks (team) {
export default async function processTeamsCron () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
}, { cron: 1, leader: 1, purchased: 1 }).exec();
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const cronPromises = activeTeams.map(updateTeamTasks);
return Promise.all(cronPromises);

View File

@@ -44,6 +44,7 @@ describe('bug-report', () => {
USER_HOURGLASSES: 0,
USER_ID: userId,
USER_LEVEL: 1,
USER_OFFSET_MONTHS: 0,
USER_PAYMENT_PLATFORM: undefined,
USER_SUBSCRIPTION: undefined,
USER_TIMEZONE_OFFSET: 0,

File diff suppressed because it is too large Load Diff

View File

@@ -171,23 +171,23 @@ describe('emails', () => {
expect(got.post).not.to.be.called;
});
it('throws error when mail target is only a string', async () => {
it('throws error when mail target is only a string', () => {
const emailType = 'an email type';
const mailingInfo = 'my email';
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when mail target has no _id or email', async () => {
it('throws error when mail target has no _id or email', () => {
const emailType = 'an email type';
const mailingInfo = {
};
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when variables not an array', async () => {
it('throws error when variables not an array', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
@@ -195,10 +195,9 @@ describe('emails', () => {
};
const variables = {};
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws error when variables array not contain name/content', async () => {
it('throws error when variables array not contain name/content', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
@@ -210,9 +209,8 @@ describe('emails', () => {
},
];
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws no error when variables array contain name but no content', () => {
const emailType = 'an email type';
const mailingInfo = {

View File

@@ -1,4 +1,5 @@
import os from 'os';
import nconf from 'nconf';
import requireAgain from 'require-again';
const pathToMongoLib = '../../../../website/server/libs/mongodb';
@@ -28,4 +29,22 @@ describe('mongodb', () => {
expect(string).to.equal('mongodb://hostname:3030');
});
});
describe('getDefaultConnectionOptions', () => {
it('returns development config when IS_PROD is false', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
});
it('returns production config when IS_PROD is true', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
});
});
});

View File

@@ -715,7 +715,7 @@ describe('Purchasing a group plan for group', () => {
const mysteryItem = { title: 'item' };
const mysteryItems = [mysteryItem];
const consecutive = {
trinkets: 4,
trinkets: 3,
gemCapExtra: 20,
offset: 1,
count: 13,

View File

@@ -12,7 +12,6 @@ import {
} from '../../../../helpers/api-unit.helper';
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
describe('payments/index', () => {
let user;
@@ -66,6 +65,7 @@ describe('payments/index', () => {
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
@@ -108,8 +108,14 @@ describe('payments/index', () => {
});
it('add a transaction entry to the recipient', async () => {
recipient.purchased.plan = plan;
expect(recipient.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
const transactions = await TransactionModel
.find({ userId: recipient._id })
.sort({ createdAt: -1 })
@@ -171,45 +177,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateUpdated).to.exist;
});
it('does not reset gemCapExtra if they already had one', async () => {
recipient.purchased.plan.consecutive.gemCapExtra = 10;
await api.createSubscription(data);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('sets gemCapExtra to 0 if they receive a 3 month sub', async () => {
data.gift.subscription.key = 'basic_3mo';
data.gift.subscription.months = 3;
await api.createSubscription(data);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
});
it('sets gemCapExtra to max if they receive a 12 month sub', async () => {
recipient.purchased.plan.consecutive.gemCapExtra = 10;
data.gift.subscription.key = 'basic_12mo';
data.gift.subscription.months = 12;
await api.createSubscription(data);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(26);
});
it('gives user 1 hourglass if they have no active subscription', async () => {
await api.createSubscription(data);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
});
it('does not give any hourglasses if they have an active subscription', async () => {
recipient.purchased.plan = plan;
await api.createSubscription(data);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(plan.consecutive.trinkets);
});
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
@@ -268,6 +235,116 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
});
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = -1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
});
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
recipient.purchased.plan.perkMonthCount = 0;
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount goes over 3', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
expect(recipient.purchased.plan.customerId).to.not.exist;
@@ -344,8 +421,8 @@ describe('payments/index', () => {
context('Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([{
...REPEATING_EVENTS.giftOneGetOne,
event: 'g1g1',
...common.content.events.winter2021Promo,
event: 'winter2021',
}]);
});
@@ -361,30 +438,22 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateTerminated).to.exist;
expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.dateCreated).to.exist;
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(recipient.purchased.plan.customerId).to.eql('Gift');
expect(recipient.purchased.plan.dateTerminated).to.exist;
expect(recipient.purchased.plan.dateUpdated).to.exist;
expect(recipient.purchased.plan.dateCreated).to.exist;
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
});
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
user.purchased.plan = plan;
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(3);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(recipient.purchased.plan.customerId).to.eql('Gift');
@@ -397,12 +466,10 @@ describe('payments/index', () => {
recipient.purchased.plan = plan;
expect(recipient.purchased.plan.extraMonths).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(user.purchased.plan.customerId).to.eql('Gift');
@@ -417,15 +484,11 @@ describe('payments/index', () => {
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(recipient.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(3);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
});
it('sends a private message about the promotion', async () => {
@@ -448,6 +511,7 @@ describe('payments/index', () => {
expect(user.purchased.plan.customerId).to.eql('customer-id');
expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.gemsBought).to.eql(0);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.dateTerminated).to.eql(null);
@@ -485,6 +549,33 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('keeps plan.perkMonthCount when changing subscription type', async () => {
await api.createSubscription(data);
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(2);
});
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('updates plan.consecutive.offset when changing subscription type', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(3);
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(6);
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
@@ -603,7 +694,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
});
});
@@ -651,20 +741,55 @@ describe('payments/index', () => {
});
context('Block subscription perks', () => {
it('adds 26 to plan.consecutive.gemCapExtra for 12 month block', async () => {
data.sub.key = 'basic_12mo';
it('adds block months to plan.consecutive.offset', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.offset).to.eql(3);
});
it('does not raise plan.consecutive.gemCapExtra higher than 26', async () => {
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('does not raise plan.consecutive.gemCapExtra higher than 25', async () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
@@ -673,29 +798,20 @@ describe('payments/index', () => {
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
});
it('adds 1 plan.consecutive.trinkets for 6 month block', async () => {
it('adds 2 plan.consecutive.trinkets for 6 month block', async () => {
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('adds 1 plan.consecutive.trinkets for 12 month block if they had promo', async () => {
user.purchased.plan.hourglassPromoReceived = new Date();
it('adds 4 plan.consecutive.trinkets for 12 month block', async () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
});
it('adds 12 plan.consecutive.trinkets for 12 month block', async () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
context('Upgrades subscription', () => {
@@ -703,38 +819,70 @@ describe('payments/index', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payDifference' };
});
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -746,7 +894,7 @@ describe('payments/index', () => {
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
@@ -754,39 +902,70 @@ describe('payments/index', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payFull' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -798,7 +977,7 @@ describe('payments/index', () => {
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
@@ -809,13 +988,30 @@ describe('payments/index', () => {
data.updatedFrom = { logic: 'refundAndRepay' };
});
context('Upgrades within first half of subscription', () => {
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-10'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
@@ -823,10 +1019,28 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-02-05'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -840,17 +1054,17 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-01-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
@@ -858,17 +1072,35 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('2 plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
@@ -876,10 +1108,10 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-08-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -893,11 +1125,11 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-07-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Upgrades within second half of subscription', () => {
it('Adds 0 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
@@ -912,16 +1144,16 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-01-20'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
@@ -929,17 +1161,17 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-02-24'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
@@ -947,17 +1179,17 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
@@ -965,10 +1197,10 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -982,17 +1214,17 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-03-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
@@ -1000,17 +1232,17 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
@@ -1018,10 +1250,10 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2023-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
@@ -1035,7 +1267,7 @@ describe('payments/index', () => {
clock = sinon.useFakeTimers(new Date('2023-09-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
afterEach(async () => {
@@ -1045,6 +1277,22 @@ describe('payments/index', () => {
});
context('Downgrades subscription', () => {
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
@@ -1052,12 +1300,28 @@ describe('payments/index', () => {
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
@@ -1067,12 +1331,12 @@ describe('payments/index', () => {
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
});
@@ -1189,32 +1453,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('does not reset gemCapExtra', async () => {
user.purchased.plan.consecutive.gemCapExtra = 12;
await api.cancelSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(12);
});
it('initializes gemCapExtra', async () => {
await api.cancelSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
});
it('initializes hourglasses', async () => {
await api.cancelSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
});
it('does not reset owned hourglasses', async () => {
user.purchased.plan.consecutive.trinkets = 12;
await api.cancelSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(12);
});
it('sends an email', async () => {
await api.cancelSubscription(data);

View File

@@ -51,7 +51,6 @@ describe('Stripe - Checkout', () => {
gift: undefined,
sub: undefined,
gemsBlock: gemsBlockKey,
server_url: BASE_URL,
};
expect(gems.validateGiftMessage).to.not.be.called;
@@ -102,7 +101,6 @@ describe('Stripe - Checkout', () => {
gift: JSON.stringify(gift),
sub: undefined,
gemsBlock: undefined,
server_url: BASE_URL,
};
expect(gems.validateGiftMessage).to.be.calledOnce;
@@ -157,7 +155,6 @@ describe('Stripe - Checkout', () => {
gift: JSON.stringify(gift),
sub: undefined,
gemsBlock: undefined,
server_url: BASE_URL,
};
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
@@ -195,7 +192,6 @@ describe('Stripe - Checkout', () => {
userId: user._id,
gift: undefined,
sub: JSON.stringify(sub),
server_url: BASE_URL,
};
expect(subscriptions.checkSubData).to.be.calledOnce;
@@ -262,7 +258,6 @@ describe('Stripe - Checkout', () => {
userId: user._id,
gift: undefined,
sub: JSON.stringify(sub),
server_url: BASE_URL,
groupId,
};
@@ -333,9 +328,8 @@ describe('Stripe - Checkout', () => {
user.purchased.plan.customerId = customerId;
const metadata = {
type: 'edit-card-user',
userId: user._id,
server_url: BASE_URL,
type: 'edit-card-user',
};
const res = await createEditCardCheckoutSession({ user }, stripe);
@@ -424,7 +418,6 @@ describe('Stripe - Checkout', () => {
const metadata = {
userId: user._id,
type: 'edit-card-group',
server_url: BASE_URL,
groupId,
};
@@ -462,7 +455,6 @@ describe('Stripe - Checkout', () => {
userId: anotherUser._id,
type: 'edit-card-group',
groupId,
server_url: BASE_URL,
};
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);

View File

@@ -308,7 +308,6 @@ describe('Stripe - One Time Payments', () => {
customerId,
paymentMethod: 'Gift',
gift,
autoRenews: false,
gemsBlock: undefined,
});
});

View File

@@ -173,7 +173,6 @@ describe('Stripe - Subscriptions', () => {
paymentMethod: 'Stripe',
sub: sinon.match({ ...sub }),
groupId: null,
autoRenews: true,
});
});
@@ -198,7 +197,6 @@ describe('Stripe - Subscriptions', () => {
paymentMethod: 'Stripe',
sub: sinon.match({ ...sub }),
groupId,
autoRenews: true,
});
});
@@ -233,7 +231,6 @@ describe('Stripe - Subscriptions', () => {
paymentMethod: 'Stripe',
sub: sinon.match({ ...sub }),
groupId,
autoRenews: true,
});
});
});

View File

@@ -16,7 +16,6 @@ import * as subscriptions from '../../../../../../website/server/libs/payments/s
const { i18n } = common;
describe('Stripe - Webhooks', () => {
const BASE_URL = nconf.get('BASE_URL');
const stripe = stripeModule('test');
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
const headers = {};
@@ -285,9 +284,7 @@ describe('Stripe - Webhooks', () => {
const session = {};
beforeEach(() => {
session.metadata = {
server_url: BASE_URL,
};
session.metadata = {};
event = { type: eventType, data: { object: session } };
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
constructEventStub.returns(event);

View File

@@ -1,11 +1,8 @@
import nconf from 'nconf';
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
} from '../../../helpers/api-unit.helper';
const authPath = '../../../../website/server/middlewares/auth';
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res; let req; let
@@ -19,7 +16,6 @@ describe('auth middleware', () => {
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', done => {
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items'],
});
@@ -39,7 +35,6 @@ describe('auth middleware', () => {
});
it('makes sure some fields are always included', done => {
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: [
'items', 'auth.timestamps',
@@ -65,57 +60,5 @@ describe('auth middleware', () => {
return done();
});
});
it('errors with InvalidCredentialsError and code when token is wrong', done => {
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
const authWithHeaders = authWithHeadersFactory({ userFieldsToExclude: [] });
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = 'totally-wrong-token';
authWithHeaders(req, res, err => {
expect(err).to.exist;
expect(err.name).to.equal('InvalidCredentialsError');
expect(err.code).to.equal('invalid_credentials');
expect(err.message).to.equal(res.t('invalidCredentials'));
return done();
});
});
describe('when ENFORCE_CLIENT_HEADER is true', () => {
let authFactory;
beforeEach(() => {
sandbox.stub(nconf, 'get').withArgs('ENFORCE_CLIENT_HEADER').returns('true');
authFactory = requireAgain(authPath).authWithHeaders;
});
it('errors with missingClientHeader when x-client header is not present', done => {
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user;
authWithHeaders(req, res, err => {
expect(err).to.exist;
expect(err.name).to.equal('BadRequest');
expect(err.message).to.equal(res.t('missingClientHeader'));
return done();
});
});
it('allows request to pass when x-client header is present', done => {
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
req.headers['x-client'] = 'habitica-web';
authWithHeaders(req, res, err => {
if (err) return done(err);
expect(res.locals.user).to.exist;
return done();
});
});
});
});
});

View File

@@ -0,0 +1,332 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import {
generateRes,
generateReq,
generateTodo,
generateDaily,
} from '../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../website/server/middlewares/cron';
import { model as User } from '../../../../website/server/models/user';
import { model as Group } from '../../../../website/server/models/group';
import * as Tasks from '../../../../website/server/models/task';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
import * as cronLib from '../../../../website/server/libs/cron';
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
describe('cron middleware', () => {
let res; let
req;
let user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analyticsService;
});
afterEach(() => {
sandbox.restore();
});
it('calls next when user is not attached', done => {
res.locals.user = null;
cronMiddleware(req, res, done);
});
it('calls next when days have not been missed', done => {
cronMiddleware(req, res, done);
});
it('should clear todos older than 30 days for free users', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
resolve();
});
return null;
});
});
});
it('should not clear todos older than 30 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.exist;
return resolve();
});
return null;
});
});
});
it('should clear todos older than 90 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
return resolve();
});
return null;
});
});
});
it('should call next if user was not modified after cron', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(hpBefore).to.equal(user.stats.hp);
return resolve();
});
});
});
it('runs cron if previous cron was incomplete', async () => {
user.lastCron = moment(new Date()).subtract({ days: 1 });
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('does damage for missing dailies', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
});
});
});
it('updates tasks', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const todo = generateTodo(user);
const todoValueBefore = todo.value;
await Promise.all([todo.save(), user.save()]);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
expect(todoFound.value).to.be.lessThan(todoValueBefore);
return resolve();
});
});
});
});
it('applies quest progress', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
const questKey = 'dilatory';
user.party.quest.key = questKey;
const party = new Group({
type: 'party',
name: generateUUID(),
leader: user._id,
});
party.quest.members[user._id] = true;
party.quest.key = questKey;
await party.save();
user.party._id = party._id;
await user.save();
party.startQuest(user);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
});
});
});
it('recovers from failed cron and does not error when user is already cronning', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
const updatedUser = user.toObject();
updatedUser.matchedCount = 0;
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne')
.withArgs({
_id: user._id,
$or: [
{ _cronSignature: 'NOT_RUNNING' },
{ _cronSignature: { $lt: sinon.match.number } },
],
})
.returns({
exec () {
return Promise.resolve(updatedUser);
},
});
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(cronLib.recoverCron).to.be.calledOnce;
return resolve();
});
});
});
it('cronSignature less than an hour ago should error', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (!err) return reject(new Error('Cron should have failed.'));
expect(err.message).to.be.equal(expectedErrMessage);
return resolve();
});
});
});
it('cronSignature longer than an hour ago should allow cron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
return resolve();
});
});
});
it('cron should not run more than once', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
sandbox.spy(cronLib, 'cron');
await Promise.all([new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
setTimeout(() => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}, 400);
}),
]);
expect(cronLib.cron).to.be.calledOnce;
});
});

View File

@@ -54,7 +54,6 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -72,7 +71,6 @@ describe('rateLimiter middleware', () => {
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
sandbox.stub(logger, 'error');
sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.')));
@@ -106,7 +104,6 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'das';
@@ -123,7 +120,6 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -139,7 +135,6 @@ describe('rateLimiter middleware', () => {
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = '';
@@ -155,7 +150,6 @@ describe('rateLimiter middleware', () => {
it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
// call for 31 times
@@ -179,7 +173,6 @@ describe('rateLimiter middleware', () => {
it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
@@ -206,51 +199,4 @@ describe('rateLimiter middleware', () => {
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/register';
req.ip = 1;
await attachRateLimiter(req, res, next);
req.headers['x-api-user'] = 'user-1';
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
// user id an ip are counted as separate sources
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 27, // 2 calls with user id
'X-RateLimit-Reset': sinon.match(Date),
});
req.headers['x-api-user'] = undefined;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 24, // 3 calls with only ip
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 10,
'X-RateLimit-Reset': sinon.match(Date),
});
});
});

View File

@@ -1,73 +0,0 @@
import nconf from 'nconf';
import {
generateUser,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/boss-rage', () => {
let user;
let nconfStub;
beforeEach(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('errors if user is not in a party', async () => {
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 400,
error: 'BadRequest',
message: 'User not in a party.',
});
});
it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
context('user is in a party', async () => {
let party;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
},
members: 2,
});
party = group;
user = groupLeader;
});
it('increases boss rage to 50', async () => {
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(50);
});
it('increases boss rage to 100', async () => {
await user.post('/debug/boss-rage');
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(100);
});
});
});

View File

@@ -34,11 +34,9 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
const tomorrow = new Date(today.valueOf());
tomorrow.setDate(today.getDate() + 1);
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('jumps back', async () => {
@@ -47,11 +45,9 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
const yesterday = new Date(today.valueOf());
yesterday.setDate(today.getDate() - 1);
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('can jump a lot', async () => {
@@ -59,7 +55,7 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 365 })).time);
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
});

View File

@@ -85,6 +85,22 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 1);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: guild.name,
}),
};
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
@@ -139,6 +155,23 @@ describe('POST /group/:groupId/join', () => {
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: party.name,
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);

View File

@@ -10,7 +10,6 @@ describe('GET /heroes/:heroId', () => {
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
'stats',
];
before(async () => {

View File

@@ -11,7 +11,6 @@ describe('PUT /heroes/:heroId', () => {
const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
'stats',
];
before(async () => {
@@ -61,12 +60,12 @@ describe('PUT /heroes/:heroId', () => {
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
expect(heroRes.contributor.level).to.equal(1);
expect(heroRes.purchased.ads).to.equal(true);
// test hero values
await hero.sync();
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
expect(hero.contributor.level).to.equal(1);
expect(hero.purchased.ads).to.equal(true);
expect(hero.auth.blocked).to.equal(prevBlockState);
@@ -137,12 +136,12 @@ describe('PUT /heroes/:heroId', () => {
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
expect(heroRes.contributor.level).to.equal(6);
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
// test hero values
await hero.sync();
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
expect(hero.contributor.level).to.equal(6);
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
});

View File

@@ -1,56 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import common from '../../../../../website/common';
describe('GET /members/username/:username', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.username', async () => {
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns a member\'s public data only', async () => {
// make sure user has all the fields that can be returned by the getMember call
const member = await generateUser({
contributor: { level: 1 },
backer: { tier: 3 },
preferences: {
costume: false,
background: 'volcano',
},
secret: {
text: 'Clark Kent',
},
});
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
expect(memberRes.inbox.optOut).to.exist;
expect(memberRes.inbox.canReceive).to.exist;
expect(memberRes.inbox.messages).to.not.exist;
expect(memberRes.secret).to.not.exist;
expect(memberRes.blocks).to.not.exist;
});
});

View File

@@ -101,6 +101,34 @@ describe('GET /tasks/user', () => {
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
});
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
const LIMIT = 30;
const numberOfTodos = LIMIT + 1;
const todosInput = [];
for (let i = 0; i < numberOfTodos; i += 1) {
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
}
const todos = await user.post('/tasks/user', todosInput);
await user.sync();
const initialTodoCount = user.tasksOrder.todos.length;
for (let i = 0; i < numberOfTodos; i += 1) {
const id = todos[i]._id;
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
}
await user.sync();
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
const completedTodos = await user.get('/tasks/user?type=completedTodos');
expect(completedTodos.length).to.equal(LIMIT);
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
expect(allCompletedTodos.length).to.equal(numberOfTodos);
});
it('returns dailies with isDue for the date specified', async () => {
// @TODO Add required format
const startDate = moment().subtract('1', 'days').toISOString();

View File

@@ -125,90 +125,6 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(body.finalLvl).to.eql(user.stats.lvl);
});
});
context('handles drops', async () => {
let randomStub;
afterEach(() => {
randomStub.restore();
});
it('gives user a drop', async () => {
user = await generateUser({
'stats.gp': 100,
'achievements.completedTask': true,
'items.eggs': {
Wolf: 1,
},
});
randomStub = sandbox.stub(Math, 'random').returns(0.1);
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
const res = await user.post(`/tasks/${task.id}/score/up`);
expect(res._tmp.drop).to.be.ok;
});
it('does not give a drop when non-sub drop cap is reached', async () => {
user = await generateUser({
'stats.gp': 100,
'achievements.completedTask': true,
'items.eggs': {
Wolf: 1,
},
'items.lastDrop.count': 5,
});
randomStub = sandbox.stub(Math, 'random').returns(0.1);
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
const res = await user.post(`/tasks/${task.id}/score/up`);
expect(res._tmp.drop).to.be.undefined;
});
it('gives a drop when subscriber is over regular cap but under subscriber cap', async () => {
user = await generateUser({
'stats.gp': 100,
'achievements.completedTask': true,
'items.eggs': {
Wolf: 1,
},
'items.lastDrop.count': 6,
'purchased.plan.customerId': '123',
});
randomStub = sandbox.stub(Math, 'random').returns(0.1);
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
const res = await user.post(`/tasks/${task.id}/score/up`);
expect(res._tmp.drop).to.be.ok;
});
it('does not give a drop when subscriber is at subscriber drop cap', async () => {
user = await generateUser({
'stats.gp': 100,
'achievements.completedTask': true,
'items.eggs': {
Wolf: 1,
},
'items.lastDrop.count': 10,
'purchased.plan.customerId': '123',
});
randomStub = sandbox.stub(Math, 'random').returns(0.1);
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
const res = await user.post(`/tasks/${task.id}/score/up`);
expect(res._tmp.drop).to.be.undefined;
});
});
});
context('todos', () => {

View File

@@ -105,9 +105,9 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const lastNotification = member.notifications[member.notifications.length - 1];
expect(lastNotification.type).to.equal('GROUP_TASK_ASSIGNED');
expect(lastNotification.taskId).to.equal(groupTask._id);
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[1].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => {

View File

@@ -89,12 +89,10 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
});
it('removes task assignment notification from unassigned user', async () => {
await member.sync();
const oldNotificationCount = member.notifications.length;
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(oldNotificationCount - 1);
expect(member.notifications.length).to.equal(1); // mystery items
});
it('unassigns a user and only that user from a task', async () => {

View File

@@ -238,28 +238,6 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
expect(isPassValid).to.equal(true);
});
it('changes the apiToken on password reset', async () => {
const user = await generateUser();
const previousToken = user.apiToken;
const code = encrypt(JSON.stringify({
userId: user._id,
expiresAt: moment().add({ days: 1 }),
}));
await user.updateOne({
'auth.local.passwordResetCode': code,
});
await api.post(`${endpoint}`, {
newPassword: 'my new password',
confirmPassword: 'my new password',
code,
});
await user.sync();
expect(user.apiToken).to.not.eql(previousToken);
});
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
const user = await generateUser();

View File

@@ -27,30 +27,11 @@ describe('PUT /user/auth/update-password', async () => {
newPassword,
confirmPassword: newPassword,
});
expect(response).to.exist;
expect(response.apiToken).to.exist;
expect(response).to.eql({});
await user.sync();
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
});
it('should change the apiToken on password change', async () => {
const previousToken = user.apiToken;
const response = await user.put(ENDPOINT, {
password,
newPassword,
confirmPassword: newPassword,
});
const newToken = response.apiToken;
expect(newToken).to.exist;
await user.sync();
expect(user.apiToken).to.eql(newToken);
expect(user.apiToken).to.not.eql(previousToken);
});
it('returns an error when confirmPassword does not match newPassword', async () => {
await expect(user.put(ENDPOINT, {
password,

View File

@@ -31,7 +31,7 @@ describe('POST /user/buy-mystery-set/:key', () => {
expect(res.data).to.eql({
items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
purchasedPlanConsecutive: JSON.parse(JSON.stringify(user.purchased.plan.consecutive)),
purchasedPlanConsecutive: user.purchased.plan.consecutive,
});
expect(res.message).to.equal(t('hourglassPurchaseSet'));
});

View File

@@ -1,104 +0,0 @@
import find from 'lodash/find';
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
/**
* Checks the messages array if the uniqueMessageId has the like flag
* @param {InboxMessage[]} messages
* @param {String} uniqueMessageId
* @param {String} userId
* @param {Boolean} likeStatus
*/
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
const messageToCheck = find(messages, { uniqueMessageId });
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
}
// eslint-disable-next-line mocha/no-exclusive-tests
describe('POST /inbox/like-private-message/:messageId', () => {
let userToSendMessage;
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
before(async () => {
userToSendMessage = await generateUser();
});
it('returns an error when private message is not found', async () => {
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('likes a message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
message: 'some message :)',
toUserId: receiver._id,
});
const { uniqueMessageId } = sentMessageResult.message;
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
expect(likeResult.likes[receiver._id]).to.equal(true);
const senderMessages = await userToSendMessage.get('/inbox/messages');
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
const receiversMessages = await receiver.get('/inbox/messages');
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
});
it('allows a user to like their own private message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
message: 'some message :)',
toUserId: receiver._id,
});
const { uniqueMessageId } = sentMessageResult.message;
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
const messages = await userToSendMessage.get('/inbox/messages');
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
const receiversMessages = await receiver.get('/inbox/messages');
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
});
it('unlikes a message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
message: 'some message :)',
toUserId: receiver._id,
});
const { uniqueMessageId } = sentMessageResult.message;
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
expect(likeResult.likes[receiver._id]).to.equal(true);
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
expect(unlikeResult.likes[receiver._id]).to.equal(false);
const messages = await userToSendMessage.get('/inbox/messages');
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
expect(messageToCheck.likes[receiver._id]).to.equal(false);
});
});

View File

@@ -40,24 +40,6 @@ describe('GET /user', () => {
expect(returnedUser.stats).to.not.exist;
});
it('returns when ALWAYS_LOADED paths are requested', async () => {
const returnedUser = await user.get('/user?userFields=_id,notifications,preferences,auth,flags,permissions');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.notifications).to.exist;
expect(returnedUser.preferences).to.exist;
expect(returnedUser.auth).to.exist;
expect(returnedUser.flags).to.exist;
expect(returnedUser.permissions).to.exist;
});
it('returns when subpaths paths are requested', async () => {
const returnedUser = await user.get('/user?userFields=auth.local.username');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.auth.local.username).to.exist;
});
it('does not return requested private properties', async () => {
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');

View File

@@ -183,6 +183,8 @@ describe('cron utility functions', () => {
});
describe('getPlanContext', () => {
const now = new Date(2022, 5, 1);
function baseUserData (count, offset, planId) {
return {
purchased: {
@@ -190,7 +192,7 @@ describe('cron utility functions', () => {
consecutive: {
count,
offset,
gemCapExtra: 26,
gemCapExtra: 25,
trinkets: 19,
},
quantity: 1,
@@ -211,19 +213,52 @@ describe('cron utility functions', () => {
};
}
it('elapsedMonths is 0 if its the same month', () => {
it('monthly plan, next date in 3 months', () => {
const user = baseUserData(60, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 0;
const planContext = getPlanContext(user, new Date(2022, 4, 20));
expect(planContext.elapsedMonths).to.equal(0);
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
});
it('elapsedMonths is 1 after one month', () => {
const user = baseUserData(60, 0, 'group_plan_auto');
it('monthly plan, next date in 1 month', () => {
const user = baseUserData(62, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, new Date(2022, 5, 11));
const planContext = getPlanContext(user, now);
expect(planContext.elapsedMonths).to.equal(1);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
});
it('multi-month plan, no offset', () => {
const user = baseUserData(60, 0, 'basic_3mo');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
});
it('multi-month plan with offset', () => {
const user = baseUserData(60, 1, 'basic_3mo');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
it('multi-month plan with perk count', () => {
const user = baseUserData(60, 1, 'basic_3mo');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
});
});

View File

@@ -47,17 +47,15 @@ describe('shops', () => {
describe('premium hatching potions', () => {
it('contains current scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(3);
expect(potions.items.length).to.eql(2);
});
it('does not contain past scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length, 'Aquatic or Celestial found').to.eql(0);
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
});
it('returns end date for scheduled premium potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.forEach(potion => {
@@ -75,9 +73,9 @@ describe('shops', () => {
});
it('does not contain locked quest premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(3);
expect(potions.items.length).to.eql(2);
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
});

View File

@@ -10,7 +10,7 @@ describe('events', () => {
});
it('returns empty array when no events are active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-11'));
clock = sinon.useFakeTimers(new Date('2024-01-06'));
const events = getRepeatingEvents();
expect(events).to.be.empty;
});
@@ -27,14 +27,14 @@ describe('events', () => {
it('returns nye event at beginning of the year', () => {
clock = sinon.useFakeTimers(new Date('2025-01-01'));
const events = getRepeatingEvents();
expect(events).to.have.length(2);
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
it('returns nye event at end of the year', () => {
clock = sinon.useFakeTimers(new Date('2024-12-30'));
const events = getRepeatingEvents();
expect(events).to.have.length(2);
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
});

View File

@@ -72,7 +72,7 @@ describe('food', () => {
});
it('sets canDrop for pie if it is pie season', () => {
clock = sinon.useFakeTimers(new Date(2024, 2, 15));
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Pie_') !== -1) {

View File

@@ -42,23 +42,23 @@ describe('content index', () => {
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
});
it('Releases pets when appropriate without needing restarting', () => {
it('Releases pets gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePets = content.petInfo;
expect(junePets['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-18'));
clock = sinon.useFakeTimers(new Date('2024-07-20'));
const julyPets = content.petInfo;
expect(julyPets['Chameleon-Base']).to.exist;
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
});
it('Releases mounts when appropriate without needing restarting', () => {
it('Releases mounts gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneMounts = content.mountInfo;
expect(juneMounts['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-18'));
clock = sinon.useFakeTimers(new Date('2024-07-20'));
const julyMounts = content.mountInfo;
expect(julyMounts['Chameleon-Base']).to.exist;
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
@@ -131,7 +131,7 @@ describe('content index', () => {
});
it('marks pie as buyable and droppable during pi day', () => {
clock = sinon.useFakeTimers(new Date('2024-03-15'));
clock = sinon.useFakeTimers(new Date('2024-03-14'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {

View File

@@ -1,42 +0,0 @@
import {
each,
} from 'lodash';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { quests } from '../../website/common/script/content/quests';
describe('quests', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('contains basic information about each quest', () => {
each(quests, (quest, key) => {
expectValidTranslationString(quest.text);
expectValidTranslationString(quest.notes);
expectValidTranslationString(quest.completion);
expect(quest.key, key).to.equal(key);
expect(quest.category, key).to.be.a('string');
if (quest.boss) {
expectValidTranslationString(quest.boss.name);
expect(quest.boss.hp, key).to.be.a('number');
expect(quest.boss.str, key).to.be.a('number');
}
expect(quest.drop).to.be.an('object');
expect(quest.drop.gp, key).to.be.a('number');
expect(quest.drop.exp, key).to.be.a('number');
if (quest.drop.items) {
quest.drop.items.forEach(drop => {
expectValidTranslationString(drop.text);
expect(drop.type, key).to.exist;
});
}
});
});
});

View File

@@ -19,8 +19,8 @@ describe('releaseDates', () => {
});
describe('armoire', () => {
it('should only contain valid armoire names', () => {
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-22`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-22`));
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
});
@@ -40,8 +40,8 @@ describe('releaseDates', () => {
describe('eggs', () => {
it('should only contain valid egg names', () => {
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
});
@@ -61,8 +61,8 @@ describe('releaseDates', () => {
describe('hatchingPotions', () => {
it('should only contain valid potion names', () => {
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
});

View File

@@ -1,5 +1,4 @@
// eslint-disable-next-line max-len
import maxBy from 'lodash/maxBy';
import moment from 'moment';
import nconf from 'nconf';
import {
@@ -11,7 +10,6 @@ import QUEST_BUNDLES from '../../website/common/script/content/bundles';
import potions from '../../website/common/script/content/hatching-potions';
import SPELLS from '../../website/common/script/content/spells';
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
import { HATCHING_POTIONS_RELEASE_DATES } from '../../website/common/script/content/constants/releaseDates';
function validateMatcher (matcher, checkedDate) {
expect(matcher.end).to.be.a('date');
@@ -20,19 +18,12 @@ function validateMatcher (matcher, checkedDate) {
describe('Content Schedule', () => {
let switchoverTime;
let clock;
beforeEach(() => {
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
clearCachedMatchers();
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('assembles scheduled items on january 15th', () => {
const date = new Date('2024-01-15');
const matchers = getAllScheduleMatchingGroups(date);
@@ -114,14 +105,8 @@ describe('Content Schedule', () => {
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day before switchover', () => {
const date = new Date('2024-05-07T07:00:00.000+00:00');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day after switchover', () => {
const date = new Date('2024-05-07T09:00:00.000+00:00');
it('sets the end date if its on the release day', () => {
const date = new Date('2024-05-07T07:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
@@ -133,64 +118,22 @@ describe('Content Schedule', () => {
});
it('sets the end date for a gala', () => {
const date = new Date('2024-05-31');
const date = new Date('2024-05-20');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date for a winter gala', () => {
const date = new Date('2025-02-28');
const date = new Date('2024-12-22');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date in new year for a winter gala', () => {
const date = new Date('2025-02-28');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('uses correct date for first hours of the month', () => {
// if the date is checked before CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the previous month
const date = new Date('2024-05-01T02:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.true;
expect(matchers.timeTravelers.match('202404'), '202404').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.false;
});
it('uses correct date after switchover time', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
const date = new Date('2024-05-01T09:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
});
it('uses UTC timezone', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
clock = sinon.useFakeTimers(new Date('2024-05-01T05:00:00.000-04:00'));
const matchers = getAllScheduleMatchingGroups();
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('contains content for repeating events', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
@@ -230,8 +173,6 @@ describe('Content Schedule', () => {
});
it('premium hatching potions', () => {
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month}-${lastReleaseDate.day + 1}`));
const potionKeys = Object.keys(potions.premium);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
@@ -272,21 +213,6 @@ describe('Content Schedule', () => {
expect(matcher.match('backgroundkey072024')).to.be.true;
});
it('allows background matching the month for new backgrounds from multiple years', () => {
const date = new Date('2026-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey072024')).to.be.true;
expect(matcher.match('backgroundkey072025')).to.be.true;
expect(matcher.match('backgroundkey072026')).to.be.true;
});
it('allows background matching the previous month in the first week for new backgrounds', () => {
const date = new Date('2024-09-02');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey082024')).to.be.true;
expect(matcher.match('backgroundkey092024')).to.be.false;
});
it('disallows background in the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
@@ -306,26 +232,19 @@ describe('Content Schedule', () => {
expect(matcher.match('backgroundkey022021')).to.be.true;
});
it('allows even yeared backgrounds in first half of year', () => {
it('allows background even yeared backgrounds in first half of year', () => {
const date = new Date('2025-02-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022024')).to.be.true;
expect(matcher.match('backgroundkey082022')).to.be.true;
});
it('allows odd yeared backgrounds in second half of year', () => {
it('allows background odd yeared backgrounds in second half of year', () => {
const date = new Date('2024-08-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022023')).to.be.true;
expect(matcher.match('backgroundkey082021')).to.be.true;
});
it('allows odd yeared backgrounds in beginning of january', () => {
const date = new Date('2025-01-06');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey122024'), 'backgroundkey122024').to.be.true;
expect(matcher.match('backgroundkey062023'), 'backgroundkey062022').to.be.true;
});
});
describe('timeTravelers matcher', () => {

View File

@@ -18,7 +18,7 @@ describe('Shop Featured Items', () => {
});
it('contains the current premium hatching potions', () => {
clock = Sinon.useFakeTimers(new Date('2024-04-09'));
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
const items = featuredItems.market();
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
});

View File

@@ -45,7 +45,7 @@ describe('time-travelers store', () => {
describe('on may 1st', () => {
beforeEach(() => {
date = new Date('2024-05-01T09:00:00.000Z');
date = new Date('2024-05-01');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);

View File

@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(global.sinon);
global.sandbox = sinon.createSandbox();
const setupNconf = require('../../website/server/libs/setupNconf').default;
const setupNconf = require('../../website/server/libs/setupNconf');
setupNconf('./config.json.example');

View File

@@ -74,10 +74,15 @@ export async function getDocument (collectionName, doc) {
}
before(done => {
mongoose.connection.once('open', async err => {
if (err) throw err;
await resetHabiticaDB();
done();
mongoose.connection.on('open', err => {
if (err) return done(err);
return resetHabiticaDB()
.then(() => {
done();
})
.catch(error => {
throw error;
});
});
});

View File

@@ -3,7 +3,7 @@
const nconf = require('nconf');
const mongoose = require('mongoose');
const setupNconf = require('../../website/server/libs/setupNconf').default;
const setupNconf = require('../../website/server/libs/setupNconf');
// fix further imports of require/import syntaxes
require('@babel/register');

View File

@@ -3,12 +3,11 @@ module.exports = {
root: true,
env: {
node: true,
es2021: true,
},
extends: [
'habitrpg/lib/vue',
],
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
ignorePatterns: ['dist/', 'node_modules/'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
@@ -40,4 +39,7 @@ module.exports = {
order: ['template', 'style', 'script'],
}],
},
parserOptions: {
parser: 'babel-eslint',
},
};

View File

@@ -0,0 +1,9 @@
/* eslint-disable import/no-commonjs */
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
};

File diff suppressed because it is too large Load Diff

View File

@@ -3,26 +3,29 @@
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vite",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix .",
"lint-no-fix": "eslint --ext .js,.vue --no-fix src",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
"lint": "vue-cli-service lint .",
"lint-no-fix": "vue-cli-service lint --no-fix .",
"postinstall": "node ./scripts/npm-postinstall.js"
},
"dependencies": {
"@froxz/vite-plugin-s3": "^1.6.0",
"@vitejs/plugin-vue2": "^2.3.3",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.8",
"@vue/cli-plugin-unit-mocha": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"autoprefixer": "^10.4.20",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "7.32.0",
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
@@ -32,34 +35,32 @@
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"markdown-it": "^14.0.0",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@vitest/browser": "^3.0.5",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
"webpack": "^5.89.0"
}
}

View File

@@ -7,12 +7,23 @@
<title>Habitica - Gamify Your Life</title>
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
<meta name="smartbanner:title" content="Habitica">
<meta name="smartbanner:author" content="HabitRPG, Inc.">
<meta name="smartbanner:price" content="FREE">
<meta name="smartbanner:price-suffix-apple" content=" - On the App Store">
<meta name="smartbanner:price-suffix-google" content=" - In Google Play">
<meta name="smartbanner:icon-apple" content="/static/presskit/Logo/iOS.png">
<meta name="smartbanner:icon-google" content="/static/presskit/Logo/Android.png">
<meta name="smartbanner:button" content="VIEW">
<meta name="smartbanner:button-url-apple" content="https://itunes.apple.com/us/app/habitica-gamified-taskmanager/id994882113">
<meta name="smartbanner:button-url-google" content="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica">
<meta name="smartbanner:enabled-platforms" content="android,ios">
<meta name="smartbanner:hide-ttl" content="2592000000">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="loading-screen">
@@ -29,9 +40,10 @@
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
<script type='text/javascript' src='/api/v4/i18n/browser-script'></script>
</body>
</html>

View File

@@ -27,16 +27,72 @@
</div>
</div>
</div>
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<div v-else>
<user-main />
<div
id="app"
:class="{
'casting-spell': castingSpell,
}"
>
<!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" />
<payments-success-modal />
<sub-cancel-modal-confirm v-if="isUserLoaded" />
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
<birthday-banner />
<notifications-display />
<app-menu />
<div
class="container-fluid"
:class="{'no-margin': noMargin}"
>
<app-header />
<buyModal
:item="selectedItemToBuy || {}"
:with-pin="true"
:generic-purchase="genericPurchase(selectedItemToBuy)"
@buyPressed="customPurchase($event)"
/>
<selectMembersModal
:item="selectedSpellToBuy || {}"
:group="user.party"
@memberSelected="memberSelected($event)"
/>
<div :class="{sticky: user.preferences.stickyHeader}">
<router-view />
</div>
</div>
<app-footer v-if="!hideFooter" />
<audio
id="sound"
ref="sound"
autoplay="autoplay"
></audio>
</template>
</template>
</div>
</div>
</template>
<style lang='scss' scoped>
@import '@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
#app {
display: flex;
flex-direction: column;
overflow-x: hidden;
}
#loading-screen-inapp {
#melior {
@@ -92,7 +148,7 @@
</style>
<style lang='scss'>
@import '@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
.modal-backdrop {
opacity: .9 !important;
@@ -107,20 +163,68 @@
<script>
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import * as Analytics from '@/libs/analytics';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo';
import BirthdayBanner from './components/header/banners/birthdayBanner';
import AppFooter from './components/appFooter';
import notificationsDisplay from './components/notifications';
import snackbars from './components/snackbars/notifications';
import { mapState } from '@/libs/store';
import snackbars from '@/components/snackbars/notifications';
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
import * as Analytics from '@/libs/analytics';
import BuyModal from './components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
import { setup as setupPayments } from '@/libs/payments';
import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
import spellsMixin from '@/mixins/spells';
import {
CONSTANTS,
getLocalSetting,
removeLocalSetting,
} from '@/libs/userlocalManager';
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
name: 'App',
components: {
AppMenu,
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,
BirthdayBanner,
notificationsDisplay,
snackbars,
userMain: () => import('@/pages/user-main'),
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
},
mixins: [notifications, spellsMixin],
data () {
return {
selectedItemToBuy: null,
@@ -134,25 +238,71 @@ export default {
};
},
computed: {
...mapState(['isUserLoggedIn', 'isUserLoaded', 'notificationsRemoved']),
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
...mapState({ user: 'user.data' }),
isStaticPage () {
return this.$route.meta.requiresLogin === false;
},
castingSpell () {
return this.$store.state.spellOptions.castingSpell;
},
noMargin () {
return ['privateMessages'].includes(this.$route.name);
},
hideFooter () {
return ['privateMessages'].includes(this.$route.name);
},
},
created () {
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
this.$root.$on('playSound', sound => {
const theme = this.user.preferences.sound;
if (!theme || theme === 'off') {
return;
}
const file = `/static/audio/${theme}/${sound}`;
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
});
this.$store.watch(state => state.isUserLoaded, () => {
if (this.isUserLoaded) {
this.hideLoadingScreen();
// @TODO: I'm not sure these should be at the app level.
// Can we move these back into shop/inventory or maybe they need a lateral move?
this.$root.$on('buyModal::showItem', item => {
this.selectedItemToBuy = item;
this.$root.$emit('bv::show::modal', 'buy-modal');
});
this.$root.$on('bv::modal::hidden', event => {
if (event.componentId === 'buy-modal') {
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
}
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
this.$root.$on('selectMembersModal::showItem', item => {
this.selectedSpellToBuy = item;
this.$root.$emit('bv::show::modal', 'select-member-modal');
});
// @TODO split up this file, it's too big
loadProgressBar({
showSpinner: false,
});
axios.interceptors.response.use(response => { // Set up Response interceptors
@@ -223,10 +373,11 @@ export default {
const errorData = error.response.data;
const errorMessage = errorData.message || errorData;
const errorCode = errorData.error;
// If 'invalid_credentials' signaled, force logout
if (error.response.status === 401 && errorCode === 'invalid_credentials') {
// Check for conditions to reset the user auth
// TODO use a specific error like NotificationNotFound instead of checking for the string
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(errorMessage) !== -1) {
this.$store.dispatch('auth:logout', { redirectToLogin: true });
return null;
}
@@ -263,35 +414,81 @@ export default {
return Promise.reject(error);
});
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
Analytics.setUser();
Analytics.updateUser();
return axios.get(
'/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
this.hideLoadingScreen();
// Adjust the timezone offset
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
this.$store.dispatch('user:set', {
'preferences.timezoneOffset': browserTimezoneOffset,
});
}
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState) {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
this.$root.$emit('habitica:payment-success', appState);
}
}
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
});
}).catch(err => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
});
} else {
this.hideLoadingScreen();
}
},
beforeDestroy () {
this.$root.$off('playSound');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) document.body.removeChild(loadingScreen);
// Check if we need to show password change success message
if (sessionStorage.getItem('passwordChangeSuccess') === 'true') {
sessionStorage.removeItem('passwordChangeSuccess');
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('passwordSuccess'),
type: 'success',
timeout: true,
});
}
this.$router.onReady(() => {
if (this.isStaticPage || !this.isUserLoggedIn) {
this.hideLoadingScreen();
}
});
},
methods: {
hideLoadingScreen () {
this.loading = false;
},
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const errorMessage = error.response.data.message;
@@ -310,8 +507,57 @@ export default {
this.$store.dispatch('auth:logout', { redirectToLogin: true });
return true;
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
genericPurchase (item) {
if (!item) return false;
if (['card', 'debuffPotion'].includes(item.purchaseType)) return false;
return true;
},
customPurchase (item) {
if (item.purchaseType === 'card') {
this.selectedSpellToBuy = item;
// hide the dialog
this.$root.$emit('bv::hide::modal', 'buy-modal');
// remove the dialog from our modal-stack,
// the default hidden event is delayed
this.$root.$emit('bv::modal::hidden', {
target: {
id: 'buy-modal',
},
});
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
},
async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member);
this.selectedSpellToBuy = null;
if (this.user.party._id) {
this.$store.dispatch('party:getMembers', { forceLoad: true });
}
this.$root.$emit('bv::hide::modal', 'select-member-modal');
},
hideLoadingScreen () {
this.loading = false;
},
},
};
</script>
<style src="intro.js/minified/introjs.min.css"></style>
<style src="axios-progress-bar/dist/nprogress.css"></style>
<style src="@/assets/scss/index.scss" lang="scss"></style>
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>

View File

@@ -22,8 +22,7 @@
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
width: 68px;
height: 68px;
}
@@ -48,10 +47,6 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
}
.Pet_HatchingPotion_Cryptid {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
}
.Gems {
display:inline-block;
margin-right:5px;
@@ -177,7 +172,7 @@
height: 96px;
}
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice, .Mount_Head_Dragon-Hydra, .Mount_Body_Dragon-Hydra {
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
width: 135px;
height: 135px;
}
@@ -190,14 +185,6 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
}
.Mount_Head_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
}
.Mount_Body_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
}
.background_airship, .background_clocktower, .background_steamworks {
width: 141px;
height: 147px;

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -19,7 +19,7 @@
top: -16px !important;
}
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
@each $foolPet in $foolPets {
.Pet.Pet-FlyingPig-#{$foolPet} {

View File

@@ -1,5 +1,5 @@
@import '@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
.featured-label {
width: auto;

View File

@@ -2,7 +2,7 @@
$grid-gutter-width: 24px;
// Bootstrap and its default variables
@import '~/bootstrap/scss/bootstrap';
@import 'node_modules/bootstrap/scss/bootstrap';
// Bootstrap Vue styles
@import '~/bootstrap-vue/dist/bootstrap-vue';
@import 'node_modules/bootstrap-vue/dist/bootstrap-vue';

View File

@@ -3,9 +3,9 @@
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: bold;
line-height: 1.714;
border: 2px solid transparent;
padding: 2px 12px;
line-height: 1.71;
border: 1px solid transparent;
padding: 4px 12px;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
color: $white;
@@ -19,7 +19,7 @@
}
&:focus {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-color: $purple-400;
}
&:active, &.active:not(.btn-flat) {
@@ -30,9 +30,9 @@
cursor: default;
color: $gray-50;
opacity: 0.75;
box-shadow: none;
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
background-color: $gray-700;
border: 2px solid transparent;
border: 1px solid transparent;
.svg {
color: $gray-300;
@@ -40,7 +40,7 @@
}
&.with-icon {
height: 32px; // otherwise would something set the height to 33px
height: 2rem; // otherwise would something set the height to 33px
display: flex;
flex-direction: row;
align-items: center;
@@ -48,47 +48,40 @@
}
.btn-front {
border: none !important;
font-size: 16px;
padding: 2px 17px;
&:hover {
border: none !important;
}
line-height: 1.5;
padding: 7.5px 15.5px;
}
.btn-primary {
background: $purple-200;
border: 2px solid transparent;
line-height: 1.714;
border: 1px solid transparent;
--icon-color: #{$purple-500};
&:focus {
background: $purple-200;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-color: $purple-400;
--icon-color: #{$white};
}
&:not(:disabled):not(.disabled) {
&:hover {
background: $purple-200;
border: 2px solid transparent;
background: #5d3b9c;
border: 1px solid transparent;
--icon-color: #{$white};
}
&:active, &.active {
background: $purple-200;
border: 2px solid transparent;
box-shadow: none;
border: 1px solid transparent;
--icon-color: #{$white};
}
&:active:focus, &.active:focus {
box-shadow: none;
border: 2px solid $purple-400;
border-color: $purple-400;
}
}
@@ -101,45 +94,42 @@
.btn-secondary,
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
.show > .btn-secondary.dropdown-toggle:not(.btn-success) {
.show > .btn-secondary.dropdown-toggle:not(.btn-success)
{
background: $white;
border: 2px solid transparent;
border: 1px solid transparent;
color: $gray-50;
--icon-color: #{$gray-200};
&:focus, &:active {
background: $white;
border: 2px solid $purple-400;
color: $gray-50;
background: $white;
border-color: $purple-400;
--icon-color: #{$purple-300};
}
&:not(:disabled):not(.disabled) {
&:active, &.active {
background: $white;
border: 2px solid $purple-400;
color: $purple-300;
--icon-color: #{$purple-300};
&:focus {
color: $purple-300;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
&:active {
box-shadow: none;
border-color: $purple-400;
}
background: $white;
border: 1px solid transparent;
}
&:hover {
color: $purple-300;
background: $white !important;
border: 2px solid transparent;
border: 1px solid transparent;
--icon-color: #{$purple-300};
.svg {
@@ -161,116 +151,67 @@
.btn-danger {
background: $maroon-100;
border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
border: 1px solid transparent;
&:hover:not(:disabled):not(.disabled) {
background: $maroon-100;
border: 2px solid transparent;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
background: #e14e4e;
border: 1px solid transparent;
}
&:focus {
background: $maroon-100;
border: 2px solid $purple-400;
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
box-shadow: none;
border: 2px solid $purple-400;
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
background: $maroon-100;
border: 2px solid $purple-400;
}
}
.btn-warning {
background: $orange-10;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
color: $white !important;
&:hover:not(:disabled):not(.disabled) {
background: $orange-10;
color: $white;
}
&:focus {
background: $orange-10;
border: 2px solid $purple-400;
}
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
border: 2px solid $purple-400;
box-shadow: none;
}
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
background: $orange-10;
box-shadow: none;
border: 1px solid transparent;
}
}
.btn-success {
background: $green-50;
border: 2px solid transparent;
border: 1px solid transparent;
&:hover:not(:disabled):not(.disabled) {
background: $green-50;
border: 2px solid transparent;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
background: #32bd8a;
border: 1px solid transparent;
}
&:focus {
background: $green-50;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
border: 2px solid $purple-400;
box-shadow: none;
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
background: $green-50;
border: 2px solid $purple-400;
box-shadow: none;
border: 1px solid transparent;
}
}
.btn-info {
background: $blue-50;
border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
&:disabled {
background: $blue-50;
box-shadow: none;
}
&:hover {
border: 2px solid transparent;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
&:focus {
background: $blue-100;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
&:hover:not(:disabled):not(.disabled) {
background-color: $blue-100;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
&:active:not(:disabled):not(.disabled), &.active:not(:disabled):not(.disabled) {
background: $blue-100;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
background: $blue-50;
}
}
@@ -279,7 +220,7 @@
width: 100%;
padding: 8px;
font-size: 14px;
line-height: 1.714;
line-height: 1.43;
font-weight: bold;
text-align: center;
background: $gray-500;
@@ -297,28 +238,12 @@
box-shadow: none;
}
.btn-flat,
.dropdown > .btn-flat.dropdown-toggle:not(.btn-success),
.show > .btn-flat.dropdown-toggle:not(.btn-success) {
&.with-icon {
.svg-icon.color {
color: var(--icon-color);
}
}
}
.btn-cancel {
color: $blue-10;
}
.btn-small {
font-size: 12px;
line-height: 2;
padding: 2px 2px;
}
.btn-lg {
font-size: 1.25rem;
line-height: 1.5;
padding: .5rem 1rem;
line-height: 1.33;
padding: 4px 8px;
}

View File

@@ -1,9 +1,8 @@
.dropdown > .btn {
padding: 0.219rem 0.75rem;
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.714;
padding: 2px 12px;
}
.dropdown-toggle:hover {
@@ -34,16 +33,11 @@
}
.dropdown-menu {
border: transparent;
padding: 0px;
border: none;
border-radius: 2px;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
padding: 0;
}
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
.no-min-width {
.dropdown-menu {
min-width: 0 !important;
}
}
// shared dropdown-item styles
@@ -59,8 +53,6 @@
color: $gray-50 !important;
cursor: pointer;
--dropdown-item-hover-icon-color: #{$gray-200};
&:focus {
outline: none;
background-color: inherit;
@@ -95,7 +87,7 @@
&:not(:hover) {
.with-icon .svg-icon {
color: var(dropdown-item-hover-icon-color);
color: $gray-200;
}
}
}
@@ -121,10 +113,6 @@
}
.dropdown-icon-item {
line-height: 1;
padding-top: 2px !important;
padding-bottom: 2px !important;
.svg-icon {
margin: 0px 16px 0px 0px;
vertical-align: middle;
@@ -140,6 +128,7 @@
.dropdown-toggle {
width: 100% !important;
height: 32px;
text-align: left;
}
@@ -158,7 +147,7 @@
// selectList.vue items sizing
.selectListItem .dropdown-item {
padding: 0.25rem 1rem 0.25rem 0.75rem;
padding: 0.25rem 0.75rem;
height: 32px;
&:active, &:hover, &:focus, &.active {

View File

@@ -1,4 +1,4 @@
@import '@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
h1 {
margin-top: 0px;
@@ -44,10 +44,6 @@ ul {
color: $purple-200;
}
h4 {
color: $gray-50;
}
.body-text {
font-size: 1em;
color: $gray-10;

Some files were not shown because too many files have changed in this diff Show More