Compare commits

..

12 Commits

Author SHA1 Message Date
negue f8fd4ae350 testing api-unit job 2023-07-23 00:40:47 +02:00
negue 21f69e4d7c use more inputs as cache key 2023-07-23 00:27:03 +02:00
negue ee09b1b60e Merge remote-tracking branch 'origin/develop' into negue/ci-cache 2023-06-25 23:03:09 +02:00
negue 7f1d332441 changing workflow triggers of push to only develop and release 2023-06-25 22:39:13 +02:00
negue b370a28c7a skip builds on more jobs 2023-05-30 23:52:59 +02:00
negue 493f90fe07 fix action.yml 2023-05-30 23:36:44 +02:00
negue bbf537421a test ci lint to ignore building the server 2023-05-30 23:34:32 +02:00
negue 15cf900caa Merge remote-tracking branch 'origin/develop' into negue/ci-cache 2023-05-30 23:10:38 +02:00
negue 3c9771dd0e test cache hits 2023-05-30 23:09:20 +02:00
negue 1236b55664 fix composite steps 2023-05-09 00:46:06 +02:00
negue ad7d4ac89e checkout in job level 2023-05-09 00:38:59 +02:00
negue 094c5b54d6 cache + first composite test 2023-05-09 00:37:21 +02:00
2941 changed files with 343400 additions and 91496 deletions
+2 -7
View File
@@ -1,11 +1,6 @@
/* eslint-disable import/no-commonjs */
module.exports = {
root: true,
extends: [
'habitrpg/lib/node',
'habitrpg/lib/node'
],
rules: {
'prefer-regex-literals': 'warn',
'import/no-extraneous-dependencies': 'off',
},
};
}
@@ -0,0 +1,61 @@
name: job setup
description: 'Sets the shared steps for each job'
inputs:
node-version:
description: 'The Node version to be setup'
required: true
node-env:
description: 'Node-Env for CI'
required: true
package-install-cmd:
description: 'CI or install or custom to skip post install and unneeded builds'
required: false
default: 'ci'
install-website-package:
description: 'if package-install-cmd skipped post install, you can trigger an installation of website'
required: false
default: 'false'
website-package-install-cmd:
description: 'CI or install or custom to skip post install and unneeded builds'
required: false
default: 'ci'
runs:
using: composite
steps:
- name: Use Node.js ${{ inputs.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
- name: Cache multiple paths
id: cache-package
uses: actions/cache@v2
with:
path: |
**/node_modules
key: ${{ runner.os }}-${{ inputs.node-version }}-${{ inputs.node-env }}-${{ inputs.install-website-package }}-${{ hashFiles('**/package.json') }}
- name: Create dummy config.json
shell: bash
run: cp config.json.example config.json
- name: npm install
if: steps.cache-package.outputs.cache-hit != 'true'
shell: bash
run: |
npm ${{ inputs.package-install-cmd }}
env:
CI: true
NODE_ENV: ${{ inputs.node-env }}
- name: npm install website
if: ${{ steps.cache-package.outputs.cache-hit != 'true' && inputs.install-website-package != 'false' }}
shell: bash
working-directory: ./website/client
run: |
npm ${{ inputs.website-package-install-cmd }}
env:
CI: true
NODE_ENV: ${{ inputs.node-env }}
+186
View File
@@ -0,0 +1,186 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: "06:00"
timezone: Europe/Rome
open-pull-requests-limit: 99
ignore:
- dependency-name: express-validator
versions:
- 6.10.0
- 6.10.1
- 6.9.2
- dependency-name: "@babel/core"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.1
- 7.13.10
- 7.13.13
- 7.13.14
- 7.13.15
- 7.13.8
- dependency-name: redis
versions:
- 3.1.0
- dependency-name: stripe
versions:
- 8.134.0
- 8.135.0
- 8.137.0
- 8.138.0
- 8.140.0
- 8.142.0
- dependency-name: "@babel/register"
versions:
- 7.12.13
- 7.13.14
- 7.13.8
- dependency-name: mongoose
versions:
- 5.11.14
- 5.11.15
- 5.11.16
- 5.11.17
- 5.11.18
- 5.11.19
- 5.12.0
- 5.12.1
- 5.12.2
- 5.12.3
- dependency-name: jwks-rsa
versions:
- 1.12.3
- 2.0.1
- 2.0.2
- dependency-name: "@babel/preset-env"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.10
- 7.13.12
- 7.13.8
- 7.13.9
- dependency-name: image-size
versions:
- 0.9.4
- 0.9.5
- 0.9.7
- dependency-name: winston-loggly-bulk
versions:
- 3.2.0
- dependency-name: chai
versions:
- 4.3.0
- 4.3.3
- dependency-name: mocha
versions:
- 8.2.1
- 8.3.0
- 8.3.1
- dependency-name: "@google-cloud/trace-agent"
versions:
- 5.1.2
- dependency-name: monk
versions:
- 7.3.3
- package-ecosystem: npm
directory: "/website/client"
schedule:
interval: weekly
time: "06:00"
timezone: Europe/Rome
open-pull-requests-limit: 99
ignore:
- dependency-name: eslint-plugin-vue
versions:
- 7.5.0
- 7.6.0
- 7.7.0
- 7.8.0
- 7.9.0
- dependency-name: "@storybook/addon-knobs"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-links"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/vue"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-actions"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: core-js
versions:
- 3.10.0
- 3.10.1
- 3.9.0
- 3.9.1
- dependency-name: bootstrap
versions:
- 4.6.0
- dependency-name: y18n
versions:
- 4.0.1
- dependency-name: hellojs
versions:
- 1.18.8
- 1.19.2
- dependency-name: chai
versions:
- 4.3.0
- 4.3.3
- dependency-name: amplitude-js
versions:
- 7.4.2
- 7.4.3
- 7.4.4
- dependency-name: pug
versions:
- 3.0.2
- dependency-name: sass
versions:
- 1.32.6
- 1.32.7
- 1.32.8
- dependency-name: "@vue/test-utils"
versions:
- 1.1.2
- 1.1.3
- dependency-name: intro.js
versions:
- 3.2.1
- 3.3.1
- dependency-name: sass-loader
versions:
- 10.1.1
+95 -133
View File
@@ -1,6 +1,13 @@
name: Test
on: [push, pull_request]
on:
pull_request:
branches:
- '**'
push:
branches:
- 'develop'
- 'release'
permissions:
contents: read
@@ -10,147 +17,120 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
install-website-package: 'true'
website-package-install-cmd: 'ci --ignore-scripts'
- run: npm run lint-no-fix
apidoc:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- run: npm run apidoc
sanity:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- run: npm run test:sanity
common:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- run: npm run test:common
content:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- run: npm run test:content
api-unit:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
node-env: 'test'
package-install-cmd: 'ci'
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
- run: npm run test:api:unit
env:
REQUIRES_SERVER=true: true
@@ -158,30 +138,26 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v3:integration
env:
REQUIRES_SERVER=true: true
@@ -189,30 +165,26 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v4:integration
env:
REQUIRES_SERVER=true: true
@@ -221,24 +193,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
env:
CI: true
NODE_ENV: test
node-env: 'test'
package-install-cmd: 'ci --ignore-scripts'
install-website-package: 'true'
website-package-install-cmd: 'ci --ignore-scripts'
- run: npm run test:unit
working-directory: ./website/client
@@ -246,21 +215,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Shared Job Steps
uses: './.github/actions/shared-job-steps'
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm install
env:
CI: true
NODE_ENV: production
node-env: 'production'
+1 -3
View File
@@ -8,7 +8,7 @@ i18n_cache
apidoc/html
*.swp
.idea*
config*.json
config.json
npm-debug.log*
lib
newrelic_agent.log
@@ -40,7 +40,6 @@ yarn.lock
!.elasticbeanstalk/*.global.yml
/.vscode
habitica.code-workspace
# webstorm fake webpack for path intellisense
webpack.webstorm.config
@@ -48,4 +47,3 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data
/.nyc_output
+1 -1
View File
@@ -1 +1 @@
20
14
+30
View File
@@ -0,0 +1,30 @@
FROM node:14
ENV ADMIN_EMAIL admin@habitica.com
ENV EMAILS_COMMUNITY_MANAGER_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
ENV APPLE_AUTH_CLIENT_ID 9Q9SMRMCNN.com.habitrpg.ios.Habitica
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch release --depth 1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git config --global url."https://".insteadOf git://
RUN npm set unsafe-perm true
RUN npm install
# Start Habitica
EXPOSE 80 8080 36612
CMD ["node", "./website/transpiled-babel/index.js"]
+5 -6
View File
@@ -1,15 +1,14 @@
FROM node:20
FROM node:14
# Install global packages
RUN npm install -g gulp-cli mocha
# Copy package.json and package-lock.json into image
# Copy package.json and package-lock.json into image, then install
# dependencies.
WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"]
RUN npm install
# Copy the remaining source files in.
COPY . /usr/src/habitica
# Install dependencies
RUN npm install
RUN npm run postinstall
RUN npm run client:build
RUN gulp build:prod
+6 -12
View File
@@ -1,20 +1,14 @@
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg)
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn Gold to buy weapons and armor!
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
**Want to contribute code to Habitica?** We're always looking for assistance on any issues in our repo with the "Help Wanted" label. The wiki pages below and the additional linked pages will tell you how to start contributing code and where you can seek further help or ask questions:
**We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions:
* [Guidance for Blacksmiths](https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths) - an introduction to the technologies used and how the software is organized.
* [Setting up Habitica Locally](https://github.com/HabitRPG/habitica/wiki/Setting-Up-Habitica-for-Local-Development) - how to set up a local install of Habitica for development and testing.
**Interested in contributing to Habiticas mobile apps?** Visit the links below for our mobile repositories.
* **Android:** https://github.com/HabitRPG/habitica-android
* **iOS:** https://github.com/HabitRPG/habitica-ios
* [Setting up Habitica Locally](https://habitica.fandom.com/wiki/Setting_up_Habitica_Locally) - how to set up a local install of Habitica for development and testing on various platforms.
Habitica's code is licensed as described at https://github.com/HabitRPG/habitica/blob/develop/LICENSE
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than create an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than creating an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Creating a third-party tool?** Please review our [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines) to ensure that your tool is compliant and maintains the best experience for Habitica players.
**Have any questions about Habitica or contributing?** See the links in the [Habitica](https://habitica.com) website's Help menu. Theres FAQs, guides, and the option to reach out to us with any further questions!
**Have any questions about Habitica or its community?** See the links in the [habitica.com](https://habitica.com) website's Help menu or drop in to [Guilds > Tavern Chat](https://habitica.com/groups/tavern) to ask questions or chat socially!
+1 -7
View File
@@ -32,12 +32,10 @@
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token",
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
"MAINTENANCE_MODE": "false",
"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",
@@ -86,12 +84,8 @@
"BLOCKED_IPS": "",
"LOG_AMPLITUDE_EVENTS": "false",
"RATE_LIMITER_ENABLED": "false",
"LIVELINESS_PROBE_KEY": "",
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
"TRUSTED_DOMAINS": "https://localhost,https://habitica.com"
}
+1 -1
View File
@@ -51,7 +51,7 @@ gulp.task('build:prepare-mongo', async () => {
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
// use run-rs without --keep, kill it as soon as the replica set starts
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
const runRsProcess = spawn('run-rs', ['-v', '4.2.8', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
for await (const chunk of runRsProcess.stdout) {
const stringChunk = chunk.toString();
+5
View File
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
import nconf from 'nconf';
import repl from 'repl';
import gulp from 'gulp';
import logger from '../website/server/libs/logger';
import {
getDevelopmentConnectionUrl,
getDefaultConnectionOptions,
@@ -38,6 +39,10 @@ const improveRepl = context => {
mongoose.connect(
connectionUrl,
mongooseOptions,
err => {
if (err) throw err;
logger.info('Connected with Mongoose');
},
);
};
+2 -33
View File
@@ -42,41 +42,10 @@ function cssVarMap (sprite) {
}
}
function filterFile (file) {
if (file.relative.indexOf('Mount_Icon_') !== -1) {
return false;
}
if (file.path.indexOf('shop/') !== -1) {
return false;
}
if (file.path.indexOf('stable/eggs') !== -1) {
return false;
}
if (file.path.indexOf('stable/food') !== -1) {
return false;
}
if (file.path.indexOf('stable/potions') !== -1) {
return false;
}
if (file.relative.indexOf('shop_') === 0) {
return false;
}
if (file.relative.indexOf('icon_background') === 0) {
return false;
}
return true;
}
async function createSpritesStream (name, src) {
function createSpritesStream (name, src) {
const stream = mergeStream();
// need to import this way bc of weird dependency things
// eslint-disable-next-line global-require
const filter = require('gulp-filter');
const f = filter(filterFile);
const spriteData = gulp.src(src)
.pipe(f)
.pipe(spritesmith({
imgName: `spritesmith-${name}.png`,
cssName: `spritesmith-${name}.css`,
@@ -94,7 +63,7 @@ async function createSpritesStream (name, src) {
return stream;
}
gulp.task('sprites:main', async () => {
gulp.task('sprites:main', () => {
const mainSrc = sync('habitica-images/**/*.png');
return createSpritesStream('main', mainSrc);
});
+18 -28
View File
@@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
return done => pipe(exec(testBin(command, envVariables), options, done));
}
function integrationTestCommand (testDir) {
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
function integrationTestCommand (testDir, coverageDir) {
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
}
/* Test task definitions */
@@ -59,15 +59,13 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
mongoose.connect(connectionUrl, mongooseOptions)
.then(() => mongoose.connection.dropDatabase())
.then(() => mongoose.connection.close()).then(() => {
cb();
})
.catch(err => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
throw err;
mongoose.connect(connectionUrl, mongooseOptions, err => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
return mongoose.connection.dropDatabase(err2 => {
if (err2) return cb(err2);
return mongoose.connection.close(cb);
});
});
});
gulp.task('test:prepare:server', gulp.series('test:prepare:mongo', done => {
@@ -118,10 +116,8 @@ gulp.task('test:common:safe', gulp.series('test:prepare:build', cb => {
pipe(runner);
}));
gulp.task('test:content', gulp.series(
'test:prepare:build',
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS),
));
gulp.task('test:content', gulp.series('test:prepare:build',
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS)));
gulp.task('test:content:clean', cb => {
pipe(exec(testBin(CONTENT_TEST_COMMAND), LIMIT_MAX_BUFFER_OPTIONS, () => cb()));
@@ -146,20 +142,16 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
pipe(runner);
}));
gulp.task(
'test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit')),
);
gulp.task('test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')));
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
gulp.task('test:api-v3:integration', gulp.series(
'test:prepare:mongo',
gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v3/integration'),
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
)));
gulp.task('test:api-v3:integration:watch', () => gulp.watch([
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
@@ -172,13 +164,11 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
'LOAD_SERVER=0',
));
gulp.task('test:api-v4:integration', gulp.series(
'test:prepare:mongo',
gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v4'),
integrationTestCommand('test/api/v4', 'api-v4-integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
)));
gulp.task('test:api-v4:integration:separate-server', runInChildProcess(
'mocha test/api/v4 --recursive --require ./test/helpers/start-server',
+3 -3
View File
@@ -3,6 +3,6 @@ module.exports = {
root: false,
rules: {
'no-console': 0,
'no-use-before-define': ['error', { functions: false }],
},
};
'no-use-before-define': ['error', { functions: false }]
}
}
@@ -61,7 +61,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
// migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')},
};
@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -145,7 +145,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -95,7 +95,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
};
@@ -86,7 +86,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
};
@@ -63,7 +63,7 @@ async function updateUser (user) {
&& pets['Wolf-Shade']
&& pets['Wolf-Skeleton']
&& pets['Wolf-White']
&& pets['Wolf-Zombie']) {
&& pets['Wolf-Zombie'] {
set['achievements.polarPro'] = true;
}
}
@@ -75,7 +75,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-11-01') },
};
@@ -125,7 +125,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
};
@@ -1,79 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230718_summer_splash_orcas';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
const push = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
return;
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set['items.pets.Orca-Base'] = 5;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_pet',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Pet!',
destination: 'stable',
},
seen: false,
};
} else {
set['items.mounts.Orca-Base'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_mount',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Mount!',
destination: 'stable',
},
seen: false,
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await user.updateOne({ $set: set, $push: push }).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-06-18')},
};
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)
.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
}
};
@@ -1,72 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { model as Group } from '../../../website/server/models/group';
const guildsPerRun = 500;
const progressCount = 1000;
const guildsQuery = {
type: 'guild',
};
let count = 0;
async function updateGroup (guild) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${guild._id}`);
}
if (guild.hasActiveGroupPlan()) {
return console.warn(`Guild ${guild._id} is active Group Plan`);
}
const leader = await User
.findOne({ _id: guild.leader })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`Leader not found for Guild ${guild._id}`);
}
if (guild.balance > 0) {
await leader.updateBalance(
guild.balance,
'create_guild',
'',
`Guild Bank refund for ${guild.name} (${guild._id})`,
);
}
return guild.updateOne({ $set: { balance: 0 } }).exec();
}
export default async function processGroups () {
const guildFields = {
_id: 1,
balance: 1,
leader: 1,
name: 1,
purchased: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundGroups = await Group // eslint-disable-line no-await-in-loop
.find(guildsQuery)
.limit(guildsPerRun)
.sort({ _id: 1 })
.select(guildFields)
.exec();
if (foundGroups.length === 0) {
console.warn('All appropriate Guilds found and modified.');
console.warn(`\n${count} Guilds processed\n`);
break;
} else {
guildsQuery._id = {
$gt: foundGroups[foundGroups.length - 1],
};
}
await Promise.all(foundGroups.map(guild => updateGroup(guild))); // eslint-disable-line no-await-in-loop
}
};
@@ -1,62 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { TransactionModel as Transaction } from '../../../website/server/models/transaction';
const transactionsPerRun = 500;
const progressCount = 1000;
const transactionsQuery = {
transactionType: 'create_guild',
amount: { $gt: 0 },
};
let count = 0;
async function updateTransaction (transaction) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
const leader = await User
.findOne({ _id: transaction.userId })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`User not found for transaction ${transaction._id}`);
}
return leader.updateOne(
{ $inc: { balance: transaction.amount }},
).exec();
}
export default async function processTransactions () {
const transactionFields = {
_id: 1,
userId: 1,
currency: 1,
amount: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await Transaction // eslint-disable-line no-await-in-loop
.find(transactionsQuery)
.limit(transactionsPerRun)
.sort({ _id: 1 })
.select(transactionFields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
} else {
transactionsQuery._id = {
$gt: foundTransactions[foundTransactions.length - 1],
};
}
await Promise.all(foundTransactions.map(txn => updateTransaction(txn))); // eslint-disable-line no-await-in-loop
}
};
@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230808_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger.',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf.',
destination: '/inventory/stable',
},
seen: false,
});
}
if (user.contributor.level > 0) {
set['items.gear.owned.armor_special_heroicTunic'] = true;
set['items.gear.owned.back_special_heroicAureole'] = true;
set['items.gear.owned.headAccessory_special_heroicCirclet'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'heroic_set_icon',
title: 'Youve received the Heroic Set!',
text: 'To commemorate your hard work as a contributor, weve awarded you the Heroic Circlet, Heroic Aureole, and Heroic Tunic.',
destination: '/inventory/equipment',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 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
}
};
@@ -1,118 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231017_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Armadillo-Base']
&& pets['Armadillo-CottonCandyBlue']
&& pets['Armadillo-CottonCandyPink']
&& pets['Armadillo-Desert']
&& pets['Armadillo-Golden']
&& pets['Armadillo-Red']
&& pets['Armadillo-Shade']
&& pets['Armadillo-Skeleton']
&& pets['Armadillo-White']
&& pets['Armadillo-Zombie']
&& pets['Cactus-Base']
&& pets['Cactus-CottonCandyBlue']
&& pets['Cactus-CottonCandyPink']
&& pets['Cactus-Desert']
&& pets['Cactus-Golden']
&& pets['Cactus-Red']
&& pets['Cactus-Shade']
&& pets['Cactus-Skeleton']
&& pets['Cactus-White']
&& pets['Cactus-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Frog-Base']
&& pets['Frog-CottonCandyBlue']
&& pets['Frog-CottonCandyPink']
&& pets['Frog-Desert']
&& pets['Frog-Golden']
&& pets['Frog-Red']
&& pets['Frog-Shade']
&& pets['Frog-Skeleton']
&& pets['Frog-White']
&& pets['Frog-Zombie']
&& pets['Snake-Base']
&& pets['Snake-CottonCandyBlue']
&& pets['Snake-CottonCandyPink']
&& pets['Snake-Desert']
&& pets['Snake-Golden']
&& pets['Snake-Red']
&& pets['Snake-Shade']
&& pets['Snake-Skeleton']
&& pets['Snake-White']
&& pets['Snake-Zombie']
&& pets['Spider-Base']
&& pets['Spider-CottonCandyBlue']
&& pets['Spider-CottonCandyPink']
&& pets['Spider-Desert']
&& pets['Spider-Golden']
&& pets['Spider-Red']
&& pets['Spider-Shade']
&& pets['Spider-Skeleton']
&& pets['Spider-White']
&& pets['Spider-Zombie']) {
set['achievements.duneBuddy'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-09-16') },
};
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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
@@ -1,124 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231114_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Cactus-Zombie'] > 0
&& pets['Cactus-Skeleton'] > 0
&& pets['Cactus-Base'] > 0
&& pets['Cactus-Desert'] > 0
&& pets['Cactus-Red'] > 0
&& pets['Cactus-Shade'] > 0
&& pets['Cactus-White']> 0
&& pets['Cactus-Golden'] > 0
&& pets['Cactus-CottonCandyBlue'] > 0
&& pets['Cactus-CottonCandyPink'] > 0
&& pets['Hedgehog-Zombie'] > 0
&& pets['Hedgehog-Skeleton'] > 0
&& pets['Hedgehog-Base'] > 0
&& pets['Hedgehog-Desert'] > 0
&& pets['Hedgehog-Red'] > 0
&& pets['Hedgehog-Shade'] > 0
&& pets['Hedgehog-White'] > 0
&& pets['Hedgehog-Golder'] > 0
&& pets['Hedgehog-CottonCandyBlue'] > 0
&& pets['Hedgehog-CottonCandyPink'] > 0
&& pets['Rock-Zombie'] > 0
&& pets['Rock-Skeleton'] > 0
&& pets['Rock-Base'] > 0
&& pets['Rock-Desert'] > 0
&& pets['Rock-Red'] > 0
&& pets['Rock-Shade'] > 0
&& pets['Rock-White'] > 0
&& pets['Rock-Golden'] > 0
&& pets['Rock-CottonCandyBlue'] > 0
&& pets['Rock-CottonCandyPink'] > 0 ) {
set['achievements.roughRider'] = true;
}
}
if (user && user.items && user.items.mounts) {
const mounts = user.items.mounts;
if (mounts['Cactus-Zombie']
&& mounts['Cactus-Skeleton']
&& mounts['Cactus-Base']
&& mounts['Cactus-Desert']
&& mounts['Cactus-Red']
&& mounts['Cactus-Shade']
&& mounts['Cactus-White']
&& mounts['Cactus-Golden']
&& mounts['Cactus-CottonCandyPink']
&& mounts['Cactus-CottonCandyBlue']
&& mounts['Hedgehog-Zombie']
&& mounts['Hedgehog-Skeleton']
&& mounts['Hedgehog-Base']
&& mounts['Hedgehog-Desert']
&& mounts['Hedgehog-Red']
&& mounts['Hedgehog-Shade']
&& mounts['Hedgehog-White']
&& mounts['Hedgehog-Golden']
&& mounts['Hedgehog-CottonCandyPink']
&& mounts['Hedgehog-CottonCandyBlue']
&& mounts['Rock-Zombie']
&& mounts['Rock-Skeleton']
&& mounts['Rock-Base']
&& mounts['Rock-Desert']
&& mounts['Rock-Red']
&& mounts['Rock-Shade']
&& mounts['Rock-White']
&& mounts['Rock-Golden']
&& mounts['Rock-CottonCandyPink']
&& mounts['Rock-CottonCandyBlue'] ) {
set['achievements.roughRider'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-02-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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
-87
View File
@@ -1,87 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231228_nye';
import { model as User } from '../../../website/server/models/user';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
let push = {};
if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
set['items.gear.owned.head_special_nye2023'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
set['items.gear.owned.head_special_nye2022'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
set['items.gear.owned.head_special_nye2021'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
set['items.gear.owned.head_special_nye2020'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
set['items.gear.owned.head_special_nye2019'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = true;
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = true;
} else {
set['items.gear.owned.head_special_nye'] = true;
}
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Happy New Year!',
text: 'Check your Equipment for this year\'s party hat!',
destination: 'inventory/equipment',
},
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let 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
}
};
@@ -1,102 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20240131_habit_birthday';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const inc = {
'items.food.Cake_Skeleton': 1,
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Zombie': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Red': 1,
'achievements.habitBirthdays': 1,
};
const set = {};
const push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Habit Birthday!',
text: 'Habitica turns 11 today! Enjoy free party robes and cake!',
destination: 'inventory/equipment',
},
seen: false,
},
};
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.armor_special_birthday2023 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2024'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2022 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2023'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2021 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2022'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2020 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2021'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2019 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2020'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2018 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2019'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2017 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2018'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2016 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2017'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2015 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2016'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday !== 'undefined') {
set['items.gear.owned.armor_special_birthday2015'] = true;
} else {
set['items.gear.owned.armor_special_birthday'] = true;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$inc: inc, $set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-12-23')},
};
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
}
};
@@ -1,89 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '202403_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['GuineaPig-Zombie'] > 0
&& pets['GuineaPig-Skeleton'] > 0
&& pets['GuineaPig-Base'] > 0
&& pets['GuineaPig-Desert'] > 0
&& pets['GuineaPig-Red'] > 0
&& pets['GuineaPig-Shade'] > 0
&& pets['GuineaPig-White']> 0
&& pets['GuineaPig-Golden'] > 0
&& pets['GuineaPig-CottonCandyBlue'] > 0
&& pets['GuineaPig-CottonCandyPink'] > 0
&& pets['Squirrel-Zombie'] > 0
&& pets['Squirrel-Skeleton'] > 0
&& pets['Squirrel-Base'] > 0
&& pets['Squirrel-Desert'] > 0
&& pets['Squirrel-Red'] > 0
&& pets['Squirrel-Shade'] > 0
&& pets['Squirrel-White'] > 0
&& pets['Squirrel-Golden'] > 0
&& pets['Squirrel-CottonCandyBlue'] > 0
&& pets['Squirrel-CottonCandyPink'] > 0
&& pets['Rat-Zombie'] > 0
&& pets['Rat-Skeleton'] > 0
&& pets['Rat-Base'] > 0
&& pets['Rat-Desert'] > 0
&& pets['Rat-Red'] > 0
&& pets['Rat-Shade'] > 0
&& pets['Rat-White'] > 0
&& pets['Rat-Golden'] > 0
&& pets['Rat-CottonCandyBlue'] > 0
&& pets['Rat-CottonCandyPink'] > 0 ) {
set['achievements.rodentRuler'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-02-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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
@@ -1,99 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '202405_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['LionCub-Zombie'] > 0
&& pets['LionCub-Skeleton'] > 0
&& pets['LionCub-Base'] > 0
&& pets['LionCub-Desert'] > 0
&& pets['LionCub-Red'] > 0
&& pets['LionCub-Shade'] > 0
&& pets['LionCub-White']> 0
&& pets['LionCub-Golden'] > 0
&& pets['LionCub-CottonCandyBlue'] > 0
&& pets['LionCub-CottonCandyPink'] > 0
&& pets['TigerCub-Zombie'] > 0
&& pets['TigerCub-Skeleton'] > 0
&& pets['TigerCub-Base'] > 0
&& pets['TigerCub-Desert'] > 0
&& pets['TigerCub-Red'] > 0
&& pets['TigerCub-Shade'] > 0
&& pets['TigerCub-White'] > 0
&& pets['TigerCub-Golden'] > 0
&& pets['TigerCub-CottonCandyBlue'] > 0
&& pets['TigerCub-CottonCandyPink'] > 0
&& pets['Sabretooth-Zombie'] > 0
&& pets['Sabretooth-Skeleton'] > 0
&& pets['Sabretooth-Base'] > 0
&& pets['Sabretooth-Desert'] > 0
&& pets['Sabretooth-Red'] > 0
&& pets['Sabretooth-Shade'] > 0
&& pets['Sabretooth-White'] > 0
&& pets['Sabretooth-Golden'] > 0
&& pets['Sabretooth-CottonCandyBlue'] > 0
&& pets['Sabretooth-CottonCandyPink'] > 0
&& pets['Cheetah-Zombie'] > 0
&& pets['Cheetah-Skeleton'] > 0
&& pets['Cheetah-Base'] > 0
&& pets['Cheetah-Desert'] > 0
&& pets['Cheetah-Red'] > 0
&& pets['Cheetah-Shade'] > 0
&& pets['Cheetah-White'] > 0
&& pets['Cheetah-Golden'] > 0
&& pets['Cheetah-CottonCandyBlue'] > 0
&& pets['Cheetah-CottonCandyPink'] > 0 ) {
set['achievements.cats'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-03-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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
@@ -1,149 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20240621_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Dragon-Veteran']) {
set['items.pets.Cactus-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_cactus',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Cactus and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
await user.updateBalance(
6,
'admin_update_balance',
'',
'Veteran Ladder award',
);
return await User.updateOne(
{ _id: user._id },
{ $set: set, $push: push, $inc: { balance: 6 } },
).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': { $gt: new Date('2024-05-21') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 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
}
};
@@ -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
}
};
+118
View File
@@ -0,0 +1,118 @@
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award Take This ladder items to participants in this month's challenge
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.gear.owned',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
let push;
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
set = {migration: migrationName};
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.back_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.body_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.head_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.armor_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.weapon_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', _id: monk.id()}};
} else {
set = {migration: migrationName, 'items.gear.owned.shield_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', _id: monk.id()}};
}
if (push) {
dbUsers.update({_id: user._id}, {$set: set, $push: push});
} else {
dbUsers.update({_id: user._id}, {$set: set});
}
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;
+10
View File
@@ -0,0 +1,10 @@
import csv
with open(r"/home/slappybag/Documents/SurveyScrape.csv") as f:
reader = csv.reader(f, delimiter=',', quotechar='"')
column = []
for row in reader:
if row:
column.append(row[4])
print column
+6 -8
View File
@@ -21,14 +21,12 @@ async function handOutJackalopes () {
if (user.party._id) groupList.push(user.party._id);
groupList = groupList.concat(user.guilds);
const subscribedGroup = await Group.findOne(
{
_id: { $in: groupList },
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{ _id: 1 },
);
const subscribedGroup = await Group.findOne({
_id: { $in: groupList },
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{ _id: 1 });
if (subscribedGroup) {
User.update({ _id: user._id }, { $set: { 'items.mounts.Jackalope-RoyalPurple': true } }).exec();
@@ -51,8 +51,7 @@ function getAchievementUpdate (newUser, oldUser) {
// Rebirth level
if (achievementsUpdate.rebirthLevel) {
achievementsUpdate.rebirthLevel = Math.max(
achievementsUpdate.rebirthLevel,
oldAchievements.rebirthLevel,
achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel,
);
} else if (oldAchievements.rebirthLevel) {
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;
-1
View File
@@ -16,7 +16,6 @@ async function updateUser (user) {
if (count % progressCount === 0) {
console.warn(`${count} ${user._id}`);
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 5000));
}
+1 -1
View File
@@ -51,7 +51,7 @@ 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 () {
-153
View File
@@ -1,153 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240731_naming_day';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
let set;
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') {
set = { migration: MIGRATION_NAME };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you some cake!',
destination: '/inventory/items',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_back',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Tail and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_body',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Cloak and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_head',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Helm and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_pet',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Pet and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_mount',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Mount and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
}
return user.updateOne({ $set: set, $inc: inc }).exec();
}
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-07-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)
.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]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240314_pi_day';
const MIGRATION_NAME = '20230314_pi_day';
const progressCount = 1000;
let count = 0;
+10979 -16955
View File
File diff suppressed because it is too large Load Diff
+40 -41
View File
@@ -1,90 +1,87 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.28.0",
"version": "4.274.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/register": "^7.22.5",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"babel-preset-env": "^1.7.0",
"bcrypt": "^5.1.1",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.2",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"coupon-code": "^0.4.5",
"csv-stringify": "^5.6.5",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"eslint": "^8.55.0",
"eslint-config-habitrpg": "^6.2.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.19.2",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
"glob": "^8.1.0",
"got": "^11.8.6",
"got": "^11.8.3",
"gulp": "^4.0.0",
"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",
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.0",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^7.6.3",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"nconf": "^0.12.0",
"node-gcm": "^1.0.5",
"nodemon": "^2.0.20",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport": "^0.5.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0",
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.2",
"rate-limiter-flexible": "^2.4.0",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.11",
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"sinon": "^15.2.0",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"stripe": "^12.9.0",
"superagent": "^8.0.9",
"universal-analytics": "^0.5.3",
"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"
"validator": "^13.9.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.9.0",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.6.0"
},
"private": true,
"engines": {
"node": "20",
"npm": "^10"
"node": "^14",
"npm": "^6"
},
"scripts": {
"lint": "eslint --ext .js --fix . && cd website/client && npm run lint",
@@ -97,35 +94,37 @@
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:api-v4:integration": "gulp test:api-v4:integration",
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
"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:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:nodemon": "gulp test:nodemon",
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.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": "gulp nodemon",
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:dev": "run-rs -v 4.2.8 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": "npm run client:build"
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.7.4",
"axios": "^1.3.6",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
"chalk": "^5.3.0",
"chalk": "^5.2.0",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
"mocha": "^5.1.1",
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.1.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
},
"optionalDependencies": {}
}
-2
View File
@@ -44,7 +44,6 @@ async function deleteHabiticaData (user, email) {
{ _id: user._id },
{ $set: set },
);
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await axios.delete(
`${BASE_URL}/api/v3/user`,
@@ -97,7 +96,6 @@ async function processEmailAddress (email) {
return console.log(`No users found with email address ${email}`);
}
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 1000));
return Promise.all(users.map(user => (async () => {
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
+2 -7
View File
@@ -1,5 +1,3 @@
/* eslint-disable import/no-commonjs */
module.exports = {
extends: [
'habitrpg/lib/mocha',
@@ -9,9 +7,6 @@ module.exports = {
chai: true,
expect: true,
sinon: true,
sandbox: true,
sandbox: true
},
rules: {
'import/no-extraneous-dependencies': 'off',
},
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
+3 -1
View File
@@ -26,7 +26,9 @@ describe('bug-report', () => {
_id: userId,
});
const result = await bugReportLogic(user, userMail, userMessage, userAgent);
const result = await bugReportLogic(
user, userMail, userMessage, userAgent,
);
expect(emailLib.sendTxn).to.be.called;
expect(result).to.deep.equal({
-89
View File
@@ -1,9 +1,5 @@
import fs from 'fs';
import * as contentLib from '../../../../website/server/libs/content';
import content from '../../../../website/common/script/content';
import {
generateRes,
} from '../../../helpers/api-unit.helper';
describe('contentLib', () => {
describe('CONTENT_CACHE_PATH', () => {
@@ -17,90 +13,5 @@ describe('contentLib', () => {
contentLib.getLocalizedContentResponse();
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
});
it('removes keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
expect(response.backgroundsFlat).to.not.exist;
expect(response.backgrounds).to.exist;
expect(response.dropHatchingPotions).to.not.exist;
expect(response.hatchingPotions).to.exist;
});
it('removes nested keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
expect(response.gear.tree).to.not.exist;
expect(response.gear.flat).to.exist;
});
});
it('generates a hash for a filter', () => {
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
expect(hash).to.equal('-1791877526');
});
it('serves content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', '', false);
expect(resSpy.send).to.have.been.calledOnce;
});
it('serves filtered content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
expect(resSpy.send).to.have.been.calledOnce;
});
describe('caches content', async () => {
let resSpy;
beforeEach(() => {
resSpy = generateRes();
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
}
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
});
it('does not cache requests in development mode', async () => {
contentLib.serveContent(resSpy, 'en', '', false);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
});
it('caches unfiltered requests', async () => {
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', '', true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
});
it('serves cached requests', async () => {
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en.json`,
'{"success": true, "data": {"all": {}}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', '', true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
});
it('caches filtered requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', filter, true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
});
it('serves filtered cached requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
'{"success": true, "data": {}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', filter, true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
});
});
});
+1 -1
View File
@@ -117,7 +117,7 @@ describe('Items Utils', () => {
it('converts values for owned gear to true/false', () => {
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(undefined);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
});
+1 -1
View File
@@ -44,7 +44,7 @@ describe('mongodb', () => {
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology', 'keepAlive', 'keepAliveInitialDelay']);
});
});
});
+3 -3
View File
@@ -227,7 +227,7 @@ describe('Password Utilities', () => {
expiresAt: moment().subtract({ minutes: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': code,
});
@@ -264,7 +264,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': 'invalid',
});
@@ -280,7 +280,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': code,
});
@@ -2,7 +2,7 @@ import { model as User } from '../../../../../../website/server/models/user';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import * as gems from '../../../../../../website/server/libs/payments/gems';
const { i18n } = common;
+10 -10
View File
@@ -342,12 +342,10 @@ describe('Apple Payments', () => {
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(
user,
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing,
);
nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -389,12 +387,10 @@ describe('Apple Payments', () => {
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(
user,
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing,
);
nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -521,7 +517,9 @@ describe('Apple Payments', () => {
const secondUser = new User();
await secondUser.save();
await expect(applePayments.subscribe(secondUser, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(
secondUser, receipt, headers, nextPaymentProcessing,
))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -561,7 +559,9 @@ describe('Apple Payments', () => {
const thirdUser = new User();
await thirdUser.save();
await expect(applePayments.subscribe(thirdUser, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(
thirdUser, receipt, headers, nextPaymentProcessing,
))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
+14 -2
View File
@@ -1382,6 +1382,18 @@ describe('payments/index', () => {
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
});
it('does not award mystery item when user already has the item in the mystery box', async () => {
user.purchased.plan.mysteryItems = [mayMysteryItem];
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
await api.createSubscription(data);
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
expect(user.purchased.plan.mysteryItems.push).to.be.calledWith('head_mystery_201605');
});
});
});
@@ -1587,10 +1599,10 @@ describe('payments/index', () => {
it('sends gem donation message in each participant\'s language', async () => {
// TODO using english for both users because other languages are not loaded
// for api.buyGems
await recipient.updateOne({
await recipient.update({
'preferences.language': 'en',
});
await user.updateOne({
await user.update({
'preferences.language': 'en',
});
await api.buyGems(data);
@@ -4,7 +4,7 @@ import nconf from 'nconf';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import * as gems from '../../../../../../website/server/libs/payments/gems';
const BASE_URL = nconf.get('BASE_URL');
@@ -1,4 +1,4 @@
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import common from '../../../../../../website/common';
import {
getOneTimePaymentInfo,
+184
View File
@@ -0,0 +1,184 @@
import apn from '@parse/node-apn/mock';
import _ from 'lodash';
import nconf from 'nconf';
import gcmLib from 'node-gcm'; // works with FCM notifications too
import { model as User } from '../../../../website/server/models/user';
import {
sendNotification as sendPushNotification,
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.spy();
apnSendSpy = sinon.spy();
sandbox.stub(nconf, 'get').returns('true-key');
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(apn.Provider.prototype, 'send').returns({
on: () => null,
send: apnSendSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
// TODO disabled because APN relies on a Promise
xit('uses APN for iOS devices', () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
const expectedNotification = new apn.Notification({
alert: message,
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
});
@@ -1,354 +0,0 @@
import apn from '@parse/node-apn';
import _ from 'lodash';
import nconf from 'nconf';
import admin from 'firebase-admin';
import { model as User } from '../../../../website/server/models/user';
import {
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
let sendPushNotification;
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
let updateStub;
let classStubbedInstance;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.stub().returns(Promise.resolve('success'));
apnSendSpy = sinon.stub().returns(Promise.resolve());
nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true');
classStubbedInstance = sandbox.createStubInstance(apn.Provider, {
send: apnSendSpy,
});
sandbox.stub(apn, 'Provider').returns(classStubbedInstance);
delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')];
// eslint-disable-next-line global-require
sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification;
updateStub = sandbox.stub(User, 'updateOne').resolves();
sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy }));
});
afterEach(() => {
sandbox.restore();
});
describe('validates supplied data', () => {
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
describe('sends notifications', () => {
let details;
beforeEach(() => {
details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
});
it('uses APN for iOS devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const expectedNotification = new apn.Notification({
alert: {
title,
body: message,
},
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
await sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
it('uses FCM for Android devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const expectedMessage = {
notification: {
title,
body: message,
},
data: {
identifier,
notificationIdentifier: identifier,
},
token: '123',
};
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledOnce;
expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage);
expect(apnSendSpy).to.not.have.been.called;
});
it('handles multiple devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
user.pushDevices.push({
type: 'ios',
regId: '456',
});
user.pushDevices.push({
type: 'android',
regId: '789',
});
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledTwice;
expect(apnSendSpy).to.have.been.calledOnce;
});
});
describe('handles sending errors', () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('removes unregistered fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
await clock.tick(10);
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
expect(updateStub).to.have.been.calledOnce;
});
it('removes unregistered apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'Unregistered' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'BadDeviceToken' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
});
});
+22 -13
View File
@@ -53,9 +53,11 @@ describe('cron middleware', () => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
resolve();
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(err);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve();
});
return null;
@@ -76,8 +78,10 @@ describe('cron middleware', () => {
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;
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
return resolve();
});
return null;
@@ -99,8 +103,10 @@ describe('cron middleware', () => {
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;
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve();
});
return null;
@@ -164,7 +170,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
return User.findOne({ _id: user._id }, (secondErr, updatedUser) => {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
@@ -181,7 +188,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
return Tasks.Task.findOne({ _id: todo._id }, (secondErr, todoFound) => {
if (secondErr) return reject(secondErr);
expect(todoFound.value).to.be.lessThan(todoValueBefore);
return resolve();
});
@@ -216,7 +224,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
return User.findOne({ _id: user._id }, (secondErr, updatedUser) => {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
@@ -229,7 +238,7 @@ describe('cron middleware', () => {
await user.save();
const updatedUser = user.toObject();
updatedUser.matchedCount = 0;
updatedUser.nMatched = 0;
sandbox.spy(cronLib, 'recoverCron');
@@ -260,7 +269,7 @@ describe('cron middleware', () => {
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({
await User.update({
_id: user._id,
}, {
$set: {
@@ -282,7 +291,7 @@ describe('cron middleware', () => {
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({
await User.update({
_id: user._id,
}, {
$set: {
@@ -6,7 +6,7 @@ import {
} from '../../../helpers/api-unit.helper';
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('ensure access middlewares', () => {
let res; let req; let
@@ -1,51 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelopmentMode from '../../../../website/server/middlewares/ensureDevelopmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when on production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when intentionally disabled', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when enabled and on non-production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
@@ -0,0 +1,38 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let
next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('returns not found when in production mode', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
ensureDevelpmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
@@ -1,51 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import { NotFound } from '../../../../website/server/libs/errors';
import ensureTimeTravelMode from '../../../../website/server/middlewares/ensureTimeTravelMode';
describe('timetravelMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when using production URL', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when not in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ import {
generateNext,
} from '../../../helpers/api-unit.helper';
import { Forbidden } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
function checkErrorThrown (next) {
expect(next).to.have.been.calledOnce;
+1 -116
View File
@@ -7,7 +7,7 @@ import {
generateNext,
} from '../../../helpers/api-unit.helper';
import { TooManyRequests } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
import logger from '../../../../website/server/libs/logger';
describe('rateLimiter middleware', () => {
@@ -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.')));
@@ -89,73 +87,8 @@ describe('rateLimiter middleware', () => {
expect(logger.error).to.have.been.calledWithMatch(Error, 'Rate Limiter Error');
});
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'abc';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
const calledWith = next.getCall(0).args;
expect(typeof calledWith[0] === 'undefined').to.equal(true);
expect(res.set).to.not.have.been.called;
});
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';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
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);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
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 = '';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
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 +112,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 +138,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),
});
});
});
@@ -1,37 +0,0 @@
/* eslint-disable global-require */
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
describe('requestLogHandler middleware', () => {
let res; let req; let
next;
const pathToMiddleWare = '../../../../website/server/middlewares/requestLogHandler';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches start time and request ID object to req', () => {
const middleware = requireAgain(pathToMiddleWare);
middleware.logRequestData(req, res, next);
expect(req.requestStartTime).to.exist;
expect(req.requestStartTime).to.be.a('number');
expect(req.requestIdentifier).to.exist;
expect(req.requestIdentifier).to.be.a('string');
});
it('calls next', () => {
const middleware = requireAgain(pathToMiddleWare);
const spy = sinon.spy();
middleware.logRequestData(req, res, spy);
expect(spy.calledOnce).to.be.true;
});
});
+18 -18
View File
@@ -1358,12 +1358,12 @@ describe('Group Model', () => {
describe('#sendChat', () => {
beforeEach(() => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
});
it('formats message', async () => {
const chatMessage = await party.sendChat({
it('formats message', () => {
const chatMessage = party.sendChat({
message: 'a _new_ message with *markdown*',
user: {
_id: 'user-id',
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
expect(chat.user).to.eql('user name');
});
it('formats message as system if no user is passed in', async () => {
const chat = await party.sendChat({ message: 'a system message' });
it('formats message as system if no user is passed in', () => {
const chat = party.sendChat({ message: 'a system message' });
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('updates users about new messages in party', async () => {
await party.sendChat({ message: 'message' });
it('updates users about new messages in party', () => {
party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1421,12 +1421,12 @@ describe('Group Model', () => {
});
});
it('updates users about new messages in group', async () => {
it('updates users about new messages in group', () => {
const group = new Group({
type: 'guild',
});
await group.sendChat({ message: 'message' });
group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1435,8 +1435,8 @@ describe('Group Model', () => {
});
});
it('does not send update to user that sent the message', async () => {
await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
it('does not send update to user that sent the message', () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1445,20 +1445,20 @@ describe('Group Model', () => {
});
});
it('skips sending new message notification for guilds with > 5000 members', async () => {
it('skips sending new message notification for guilds with > 5000 members', () => {
party.memberCount = 5001;
await party.sendChat({ message: 'message' });
party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called;
expect(User.update).to.not.be.called;
});
it('skips sending messages to the tavern', async () => {
it('skips sending messages to the tavern', () => {
party._id = TAVERN_ID;
await party.sendChat({ message: 'message' });
party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called;
expect(User.update).to.not.be.called;
});
});
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
await guild.save();
const groupMessage = await guild.sendChat({ message: 'Test message.' });
const groupMessage = guild.sendChat({ message: 'Test message.' });
await groupMessage.save();
await sleep();
+2 -2
View File
@@ -103,7 +103,7 @@ describe('NewsPost Model', () => {
beforeEach(async () => {
// Delete all existing posts from the database
await NewsPost.deleteMany();
await NewsPost.remove();
});
afterEach(() => {
@@ -116,7 +116,7 @@ describe('NewsPost Model', () => {
_id: v4(), publishDate: new Date(), published: true,
};
NewsPost.updateLastNewsPost(previousPost);
intervalId = refreshNewsPost(100); // refreshes every 100ms
intervalId = refreshNewsPost(50); // refreshes every 50ms
await sleep(0.1); // wait 100ms to make sure the new post has a more recent publishDate
const newPost = await NewsPost.create({
+3 -6
View File
@@ -221,8 +221,7 @@ describe('Task Model', () => {
it('returns task by alias', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias],
user._id,
[taskWithAlias.alias], user._id,
);
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
@@ -230,8 +229,7 @@ describe('Task Model', () => {
it('returns multiple tasks', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, secondTask._id],
user._id,
[taskWithAlias.alias, secondTask._id], user._id,
);
expect(foundTasks.length).to.eql(2);
@@ -241,8 +239,7 @@ describe('Task Model', () => {
it('returns a task only once if searched by both id and alias', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, taskWithAlias._id],
user._id,
[taskWithAlias.alias, taskWithAlias._id], user._id,
);
expect(foundTasks.length).to.eql(1);
+5 -5
View File
@@ -188,7 +188,7 @@ describe('User Model', () => {
it('removes invalid tags when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
tags: [
null, // invalid, not an object
@@ -212,7 +212,7 @@ describe('User Model', () => {
it('removes invalid push devices when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
pushDevices: [
null, // invalid, not an object
@@ -236,7 +236,7 @@ describe('User Model', () => {
it('removes duplicate push devices when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
pushDevices: [
{ type: 'android', regId: '1234' },
@@ -258,7 +258,7 @@ describe('User Model', () => {
it('removes invalid notifications when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
notifications: [
null, // invalid, not an object
@@ -284,7 +284,7 @@ describe('User Model', () => {
it('removes multiple NEW_CHAT_MESSAGE for the same group', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
notifications: [
{
+1 -1
View File
@@ -1,7 +1,7 @@
import { v4 as generateUUID } from 'uuid';
import { model as Webhook } from '../../../../website/server/models/webhook';
import { BadRequest } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('Webhook Model', () => {
context('Instance Methods', () => {
@@ -87,7 +87,7 @@ describe('DELETE /challenges/:challengeId', () => {
const testTask = _.find(tasks, task => task.text === taskText);
expect(testTask.challenge.broken).to.eql('CHALLENGE_DELETED');
expect(testTask.challenge.winner).to.be.undefined;
expect(testTask.challenge.winner).to.be.null;
});
});
});
@@ -16,7 +16,60 @@ describe('GET /challenges/:challengeId', () => {
});
});
context('Group Plan', () => {
context('public guild', () => {
let groupLeader;
let group;
let challenge;
let user;
beforeEach(async () => {
user = await generateUser();
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
const chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: { name: groupLeader.profile.name },
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
categories: [],
id: group.id,
name: group.name,
summary: group.name,
type: group.type,
privacy: group.privacy,
leader: groupLeader.id,
});
});
});
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
@@ -31,14 +84,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
members: 2,
upgradeToGroupPlan: true,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
[challengeLeader, otherMember] = members;
challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
challenge = await generateChallenge(challengeLeader, group);
});
@@ -74,7 +74,7 @@ describe('GET /challenges/:challengeId/export/csv', () => {
});
it('should successfully return when it contains erroneous residue user data', async () => {
await members[0].updateOne({ challenges: [] });
await members[0].update({ challenges: [] });
const res = await members[1].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
@@ -71,18 +71,42 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('populates only some fields', async () => {
const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() });
const challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
it('works with challenges belonging to public guild', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: user._id,
id: user._id,
profile: { name: user.profile.name },
_id: leader._id,
id: leader._id,
profile: { name: leader.profile.name },
auth: {
local: {
username: user.auth.local.username,
username: leader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('populates only some fields', async () => {
const anotherUser = await generateUser({ balance: 3 });
const group = await generateGroup(anotherUser, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(anotherUser, group);
await anotherUser.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
id: anotherUser._id,
profile: { name: anotherUser.profile.name },
auth: {
local: {
username: anotherUser.auth.local.username,
},
},
flags: {
@@ -72,6 +72,20 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
});
});
it('works with challenges belonging to a public guild', async () => {
const groupLeader = await generateUser({ balance: 4 });
const group = await generateGroup(groupLeader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
const taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: taskText }]);
const memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});
it('returns the member tasks for the challenges', async () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group);
@@ -7,7 +7,117 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => {
context('Group Plan', () => {
context('Public Guild', () => {
let publicGuild; let user; let nonMember; let challenge; let
challenge2;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
const challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return group challenges for member with populated leader', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return newest challenges first', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
});
context('Private Guild', () => {
let privateGuild; let user; let nonMember; let challenge; let
challenge2;
@@ -18,7 +128,6 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
privateGuild = group;
@@ -77,6 +186,68 @@ describe('GET challenges/groups/:groupId', () => {
});
});
context('official challenge is present', () => {
let publicGuild; let user; let officialChallenge; let unofficialChallenges;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
context('Party', () => {
let party; let user; let nonMember; let challenge; let
challenge2;
@@ -186,7 +357,7 @@ describe('GET challenges/groups/:groupId', () => {
before(async () => {
user = await generateUser();
await user.updateOne({ balance: 0.5 });
await user.update({ balance: 0.5 });
tavern = await user.get(`/groups/${TAVERN_ID}`);
challenge = await generateChallenge(user, tavern, { prize: 1 });
@@ -230,7 +401,7 @@ describe('GET challenges/groups/:groupId', () => {
});
});
it('should return tavern challenges using ID "habitrpg"', async () => {
it('should return tavern challenges using ID "habitrpg', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -264,58 +435,5 @@ describe('GET challenges/groups/:groupId', () => {
},
});
});
context('official challenge is present', () => {
let officialChallenge; let unofficialChallenges;
before(async () => {
await user.updateOne({
'permissions.challengeAdmin': true,
balance: 3,
});
officialChallenge = await generateChallenge(user, tavern, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
prize: 1,
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, tavern, { prize: 1 }); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get('/challenges/groups/habitrpg');
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, tavern, { prize: 1 });
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
});
});
@@ -2,44 +2,39 @@ import {
generateUser,
generateChallenge,
createAndPopulateGroup,
resetHabiticaDB,
} from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/user', () => {
context('no official challenges', () => {
let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge;
let groupPlan; let userData; let groupData; let tavern; let tavernData;
let user; let member; let nonMember; let challenge; let challenge2;
let publicGuild; let userData; let groupData;
before(async () => {
await resetHabiticaDB();
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
groupPlan = group;
publicGuild = group;
groupData = {
_id: groupPlan._id,
_id: publicGuild._id,
categories: [],
id: groupPlan._id,
type: groupPlan.type,
privacy: groupPlan.privacy,
name: groupPlan.name,
summary: groupPlan.name,
leader: groupPlan.leader._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
summary: publicGuild.name,
leader: publicGuild.leader._id,
};
user = groupLeader;
userData = {
_id: groupPlan.leader._id,
id: groupPlan.leader._id,
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
@@ -51,31 +46,17 @@ describe('GET challenges/user', () => {
},
};
tavern = await user.get(`/groups/${TAVERN_ID}`);
tavernData = {
_id: TAVERN_ID,
categories: [],
id: TAVERN_ID,
type: tavern.type,
privacy: tavern.privacy,
name: tavern.name,
summary: tavern.name,
leader: tavern.leader._id,
};
member = members[0]; // eslint-disable-line prefer-destructuring
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
await user.updateOne({ balance: 0.25 });
publicChallenge = await generateChallenge(user, tavern, { prize: 1 });
await member.post(`/challenges/${challenge._id}/join`);
await nonMember.post(`/challenges/${challenge._id}/join`);
});
context('all challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await member.get('/challenges/user?page=0');
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -83,13 +64,11 @@ describe('GET challenges/user', () => {
expect(foundChallenge.group).to.eql(groupData);
});
it('should return public challenges', async () => {
it('should not return challenges a non-member has not joined', async () => {
const challenges = await nonMember.get('/challenges/user?page=0');
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should return challenges user has created', async () => {
@@ -121,10 +100,10 @@ describe('GET challenges/user', () => {
it('should return newest challenges first', async () => {
let challenges = await user.get('/challenges/user?page=0');
let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id });
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, groupPlan);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -134,23 +113,52 @@ describe('GET challenges/user', () => {
});
it('should not return challenges user doesn\'t have access to', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group, {
categories: [{
name: 'academics',
slug: 'academics',
}],
});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
});
context('my challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -169,10 +177,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should return challenges user has created if filter by owned', async () => {
@@ -186,10 +190,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should not return challenges user has created if filter by not owned', async () => {
@@ -199,42 +199,38 @@ describe('GET challenges/user', () => {
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
it('should not return challenges in user groups', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should not return public challenges', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
});
});
context('official challenge is present', () => {
let user; let officialChallenge; let unofficialChallenges; let
group;
publicGuild;
before(async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
summary: 'summary for TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
}));
});
await user.updateOne({
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
});
@@ -275,7 +271,7 @@ describe('GET challenges/user', () => {
}
});
const newChallenge = await generateChallenge(user, group);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -298,17 +294,16 @@ describe('GET challenges/user', () => {
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
user = groupLeader;
guild = group;
member = members[0]; // eslint-disable-line prefer-destructuring
await user.updateOne({ balance: 20 });
await user.update({ balance: 20 });
for (let i = 0; i < 11; i += 1) {
let challenge = await generateChallenge(user, group); // eslint-disable-line
@@ -1,79 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/flag', () => {
let user;
let challengeGroup;
let challenge;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
user = groupLeader;
challengeGroup = group;
challenge = await generateChallenge(user, group);
});
it('returns an error when challenge is not found', async () => {
await expect(user.post(`/challenges/${generateUUID()}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('flags a challenge', async () => {
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(1);
});
it('flags a challenge with a higher count when from an admin', async () => {
await user.updateOne({ 'contributor.admin': true });
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(5);
});
it('returns an error when user tries to flag a challenge that is already flagged', async () => {
await user.post(`/challenges/${challenge._id}/flag`);
await expect(user.post(`/challenges/${challenge._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageChallengeFlagAlreadyReported'),
});
});
it('returns an error when user tries to flag an official challenge', async () => {
await user.updateOne({
permissions: {
challengeAdmin: true,
},
});
challenge = await generateChallenge(user, challengeGroup, { official: true });
await expect(user.post(`/challenges/${challenge._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageChallengeFlagOfficial'),
});
});
});
@@ -1,58 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/clearflags', () => {
let admin;
let nonAdmin;
let challenge;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
admin = groupLeader;
[nonAdmin] = members;
await admin.updateOne({ 'permissions.moderator': true });
challenge = await generateChallenge(admin, group);
await admin.post(`/challenges/${challenge._id}/flag`);
});
it('returns error when non-admin attempts to clear flags', async () => {
await expect(nonAdmin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatAdminClearFlagCount'),
});
});
it('returns an error when challenge is not found', async () => {
await expect(admin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('clears flags and sets mod flag to false', async () => {
await nonAdmin.post(`/challenges/${challenge._id}/flag`);
const flagResult = await admin.post(`/challenges/${challenge._id}/clearflags`);
expect(flagResult.challenge.flagCount).to.eql(0);
expect(flagResult.challenge.flags).to.have.property(admin._id, false);
expect(flagResult.challenge.flags).to.have.property(nonAdmin._id, true);
});
});
@@ -42,7 +42,26 @@ describe('POST /challenges', () => {
});
});
it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
it('returns error when creating a challenge in a public guild and you are not a member of it', async () => {
const user = await generateUser();
const { group } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
});
await expect(user.post('/challenges', {
group: group._id,
prize: 4,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustBeGroupMember'),
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({
@@ -58,7 +77,7 @@ describe('POST /challenges', () => {
});
});
context('creating a Challenge for a Group Plan', () => {
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
let groupMember;
@@ -75,11 +94,9 @@ describe('POST /challenges', () => {
challenges: true,
},
},
upgradeToGroupPlan: true,
});
groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.updateOne({ permissions: {} });
group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
});
@@ -177,7 +194,7 @@ describe('POST /challenges', () => {
const oldUserBalance = groupLeader.balance;
const prize = 8;
await group.updateOne({ balance: 0 });
await group.update({ balance: 0 });
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
@@ -202,7 +219,7 @@ describe('POST /challenges', () => {
});
it('sets challenge as official if created by admin and official flag is set', async () => {
await groupLeader.updateOne({
await groupLeader.update({
permissions: {
challengeAdmin: true,
},
@@ -331,71 +348,5 @@ describe('POST /challenges', () => {
expect(updatedChallenge.summary).to.eql(summary);
});
it('sets categories for challenges', async () => {
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'test', name: 'Test' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.categories).to.eql([testCategory]);
});
it('does not set habitica_official category for non-admins', async () => {
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
await expect(groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noPrivAccess'),
});
});
it('sets habitica_official category for admins', async () => {
await groupLeader.updateOne({
permissions: {
challengeAdmin: true,
},
});
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.categories).to.eql([testCategory]);
});
it('sets official if the habitica_official category is set for admins', async () => {
await groupLeader.updateOne({
permissions: {
challengeAdmin: true,
},
});
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.official).to.eql(true);
});
});
});
@@ -128,10 +128,10 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
const oldBalance = winningUser.balance;
const oldLeaderBalance = (await groupLeader.sync()).balance;
await winningUser.updateOne({
await winningUser.update({
'purchased.plan.customerId': 'group-plan',
});
await group.updateOne({
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});

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