Compare commits

..

1 Commits

Author SHA1 Message Date
Phillip Thelen
c46bc2b0e5 Disable Amplitude on client for dev environment 2024-01-22 13:48:48 +01:00
991 changed files with 63327 additions and 69814 deletions

View File

@@ -5,7 +5,6 @@ module.exports = {
'habitrpg/lib/node', 'habitrpg/lib/node',
], ],
rules: { rules: {
'prefer-regex-literals': 'warn',
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
}, },
}; };

150
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,150 @@
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: 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

View File

@@ -10,21 +10,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -33,21 +31,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -56,21 +52,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -80,21 +74,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -103,21 +95,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -127,14 +117,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
mongodb-version: [4.2] mongodb-version: [4.2]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set - name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -142,12 +132,10 @@ jobs:
with: with:
mongodb-version: ${{ matrix.mongodb-version }} mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -158,14 +146,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
mongodb-version: [4.2] mongodb-version: [4.2]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set - name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -173,12 +161,10 @@ jobs:
with: with:
mongodb-version: ${{ matrix.mongodb-version }} mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -189,14 +175,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
mongodb-version: [4.2] mongodb-version: [4.2]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set - name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -204,12 +190,10 @@ jobs:
with: with:
mongodb-version: ${{ matrix.mongodb-version }} mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -221,21 +205,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |
npm i npm ci
env: env:
CI: true CI: true
NODE_ENV: test NODE_ENV: test
@@ -246,17 +228,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [21.x] node-version: [14.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v1
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json - run: cp config.json.example config.json
- name: npm install - name: npm install
run: | run: |

4
.gitignore vendored
View File

@@ -8,7 +8,7 @@ i18n_cache
apidoc/html apidoc/html
*.swp *.swp
.idea* .idea*
config*.json config.json
npm-debug.log* npm-debug.log*
lib lib
newrelic_agent.log newrelic_agent.log
@@ -40,7 +40,6 @@ yarn.lock
!.elasticbeanstalk/*.global.yml !.elasticbeanstalk/*.global.yml
/.vscode /.vscode
habitica.code-workspace
# webstorm fake webpack for path intellisense # webstorm fake webpack for path intellisense
webpack.webstorm.config webpack.webstorm.config
@@ -48,4 +47,3 @@ webpack.webstorm.config
# mongodb replica set for local dev # mongodb replica set for local dev
mongodb-*.tgz mongodb-*.tgz
/mongodb-data /mongodb-data
/.nyc_output

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
20 14

30
Dockerfile Normal file
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"]

View File

@@ -1,15 +1,14 @@
FROM node:20 FROM node:14
# Install global packages # Install global packages
RUN npm install -g gulp-cli mocha 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 WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"] COPY ["package.json", "package-lock.json", "./"]
RUN npm install
# Copy the remaining source files in. # Copy the remaining source files in.
COPY . /usr/src/habitica COPY . /usr/src/habitica
# Install dependencies
RUN npm install
RUN npm run postinstall RUN npm run postinstall
RUN npm run client:build
RUN gulp build:prod

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. * [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. * [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.
**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
Habitica's code is licensed as described at https://github.com/HabitRPG/habitica/blob/develop/LICENSE 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 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!
**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!

View File

@@ -32,12 +32,10 @@
"LOGGLY_CLIENT_TOKEN": "token", "LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_SUBDOMAIN": "example-subdomain", "LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token", "LOGGLY_TOKEN": "example-token",
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
"MAINTENANCE_MODE": "false", "MAINTENANCE_MODE": "false",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs", "NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs", "TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"MONGODB_POOL_SIZE": "10", "MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_ENV": "development", "NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin", "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo", "PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
@@ -86,12 +84,8 @@
"BLOCKED_IPS": "", "BLOCKED_IPS": "",
"LOG_AMPLITUDE_EVENTS": "false", "LOG_AMPLITUDE_EVENTS": "false",
"RATE_LIMITER_ENABLED": "false", "RATE_LIMITER_ENABLED": "false",
"LIVELINESS_PROBE_KEY": "",
"REDIS_HOST": "aaabbbcccdddeeefff", "REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234", "REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678", "REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,https://habitica.com", "TRUSTED_DOMAINS": "localhost,habitica.com"
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
} }

View File

@@ -42,41 +42,10 @@ function cssVarMap (sprite) {
} }
} }
function filterFile (file) { function createSpritesStream (name, src) {
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) {
const stream = mergeStream(); 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) const spriteData = gulp.src(src)
.pipe(f)
.pipe(spritesmith({ .pipe(spritesmith({
imgName: `spritesmith-${name}.png`, imgName: `spritesmith-${name}.png`,
cssName: `spritesmith-${name}.css`, cssName: `spritesmith-${name}.css`,
@@ -94,7 +63,7 @@ async function createSpritesStream (name, src) {
return stream; return stream;
} }
gulp.task('sprites:main', async () => { gulp.task('sprites:main', () => {
const mainSrc = sync('habitica-images/**/*.png'); const mainSrc = sync('habitica-images/**/*.png');
return createSpritesStream('main', mainSrc); return createSpritesStream('main', mainSrc);
}); });

View File

@@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
return done => pipe(exec(testBin(command, envVariables), options, done)); return done => pipe(exec(testBin(command, envVariables), options, done));
} }
function integrationTestCommand (testDir) { function integrationTestCommand (testDir, coverageDir) {
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`; return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
} }
/* Test task definitions */ /* Test task definitions */
@@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
gulp.task( gulp.task(
'test:api:unit:run', 'test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit')), 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:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
@@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
gulp.task('test:api-v3:integration', gulp.series( gulp.task('test:api-v3:integration', gulp.series(
'test:prepare:mongo', 'test:prepare:mongo',
runInChildProcess( runInChildProcess(
integrationTestCommand('test/api/v3/integration'), integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
LIMIT_MAX_BUFFER_OPTIONS, LIMIT_MAX_BUFFER_OPTIONS,
), ),
)); ));
@@ -175,7 +175,7 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
gulp.task('test:api-v4:integration', gulp.series( gulp.task('test:api-v4:integration', gulp.series(
'test:prepare:mongo', 'test:prepare:mongo',
runInChildProcess( runInChildProcess(
integrationTestCommand('test/api/v4'), integrationTestCommand('test/api/v4', 'api-v4-integration'),
LIMIT_MAX_BUFFER_OPTIONS, LIMIT_MAX_BUFFER_OPTIONS,
), ),
)); ));

View File

@@ -61,7 +61,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: {$ne: MIGRATION_NAME}, // migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')}, 'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')},
}; };

View File

@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -145,7 +145,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -95,7 +95,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
}; };

View File

@@ -86,7 +86,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
}; };

View File

@@ -63,7 +63,7 @@ async function updateUser (user) {
&& pets['Wolf-Shade'] && pets['Wolf-Shade']
&& pets['Wolf-Skeleton'] && pets['Wolf-Skeleton']
&& pets['Wolf-White'] && pets['Wolf-White']
&& pets['Wolf-Zombie']) { && pets['Wolf-Zombie'] {
set['achievements.polarPro'] = true; set['achievements.polarPro'] = true;
} }
} }
@@ -75,7 +75,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-11-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-11-01') },
}; };

View File

@@ -125,7 +125,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') }, 'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
}; };

View File

@@ -1,12 +1,14 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { model as User } from '../../website/server/models/user'; const MIGRATION_NAME = '20230731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20240731_naming_day';
const progressCount = 1000; const progressCount = 1000;
let count = 0; let count = 0;
async function updateUser (user) { async function updateUser (user) {
count += 1; count++;
let set; let set;
let push; let push;
@@ -113,16 +115,16 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`); if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) { if (push) {
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec(); return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
} }
return user.updateOne({ $set: set, $inc: inc }).exec();
} }
export default async function processUsers () { export default async function processUsers () {
const query = { let query = {
migration: { $ne: MIGRATION_NAME }, migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-07-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
}; };
const fields = { const fields = {
@@ -134,7 +136,7 @@ export default async function processUsers () {
const users = await User // eslint-disable-line no-await-in-loop const users = await User // eslint-disable-line no-await-in-loop
.find(query) .find(query)
.limit(250) .limit(250)
.sort({ _id: 1 }) .sort({_id: 1})
.select(fields) .select(fields)
.exec(); .exec();
@@ -150,4 +152,4 @@ export default async function processUsers () {
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
} }
} };

View File

@@ -110,7 +110,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: {$ne: MIGRATION_NAME}, migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') }, // 'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
}; };
const fields = { const fields = {

View File

@@ -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
}
};

View File

@@ -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
}
};

View File

@@ -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
}
};

View File

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

View File

@@ -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
migrations/csvexport.py Normal file
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

View File

@@ -51,7 +51,7 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`); 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 () { export default async function processUsers () {

View File

@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user'; import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240314_pi_day'; const MIGRATION_NAME = '20230314_pi_day';
const progressCount = 1000; const progressCount = 1000;
let count = 0; let count = 0;

27926
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.27.3", "version": "5.16.1",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.22.10",
@@ -15,7 +15,6 @@
"amplitude": "^6.0.0", "amplitude": "^6.0.0",
"apidoc": "^0.54.0", "apidoc": "^0.54.0",
"apple-auth": "^1.0.9", "apple-auth": "^1.0.9",
"babel-preset-env": "^1.7.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.2",
@@ -28,15 +27,13 @@
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-habitrpg": "^6.2.3", "eslint-config-habitrpg": "^6.2.3",
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"express": "^4.19.2", "express": "^4.18.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
"glob": "^8.1.0", "glob": "^8.1.0",
"got": "^11.8.6", "got": "^11.8.6",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0", "gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0", "gulp.spritesmith": "^6.13.0",
@@ -69,22 +66,20 @@
"remove-markdown": "^0.5.0", "remove-markdown": "^0.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"short-uuid": "^4.2.2", "short-uuid": "^4.2.2",
"sinon": "^15.2.0",
"stripe": "^12.18.0", "stripe": "^12.18.0",
"superagent": "^8.1.2", "superagent": "^8.1.2",
"universal-analytics": "^0.5.3", "universal-analytics": "^0.5.3",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"validator": "^13.11.0", "validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0", "winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0", "winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"private": true, "private": true,
"engines": { "engines": {
"node": "20", "node": "^14",
"npm": "^10" "npm": "^6"
}, },
"scripts": { "scripts": {
"lint": "eslint --ext .js --fix . && cd website/client && npm run lint", "lint": "eslint --ext .js --fix . && cd website/client && npm run lint",
@@ -97,11 +92,11 @@
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server", "test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:api-v4:integration": "gulp test:api-v4:integration", "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: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:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "nyc --silent --no-clean mocha test/common --recursive", "test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "nyc --silent --no-clean mocha test/content --recursive", "test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:nodemon": "gulp test:nodemon", "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", "sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve", "client:dev": "cd website/client && npm run serve",
"client:build": "cd website/client && npm run build", "client:build": "cd website/client && npm run build",
@@ -110,8 +105,7 @@
"debug": "gulp nodemon --inspect", "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 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install", "postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc", "apidoc": "gulp apidoc"
"heroku-postbuild": ".heroku/report_deploy.sh"
}, },
"devDependencies": { "devDependencies": {
"axios": "^1.4.0", "axios": "^1.4.0",
@@ -120,12 +114,15 @@
"chai-moment": "^0.1.0", "chai-moment": "^0.1.0",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
"mocha": "^5.1.1", "mocha": "^5.1.1",
"monk": "^7.3.4", "monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"run-rs": "^0.7.7", "run-rs": "^0.7.7",
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0", "sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0" "sinon-stub-promise": "^4.0.0"
} },
"optionalDependencies": {}
} }

View File

@@ -1,4 +1,4 @@
import { apiError } from '../../../../website/server/libs/apiError'; import apiError from '../../../../website/server/libs/apiError';
describe('API Messages', () => { describe('API Messages', () => {
const message = 'Only public guilds support pagination.'; const message = 'Only public guilds support pagination.';

View File

@@ -1,9 +1,5 @@
import fs from 'fs';
import * as contentLib from '../../../../website/server/libs/content'; import * as contentLib from '../../../../website/server/libs/content';
import content from '../../../../website/common/script/content'; import content from '../../../../website/common/script/content';
import {
generateRes,
} from '../../../helpers/api-unit.helper';
describe('contentLib', () => { describe('contentLib', () => {
describe('CONTENT_CACHE_PATH', () => { describe('CONTENT_CACHE_PATH', () => {
@@ -17,90 +13,5 @@ describe('contentLib', () => {
contentLib.getLocalizedContentResponse(); contentLib.getLocalizedContentResponse();
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function'); 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`);
});
}); });
}); });

View File

@@ -117,7 +117,7 @@ describe('Items Utils', () => {
it('converts values for owned gear to true/false', () => { 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.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false); 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', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false); expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
}); });

View File

@@ -44,7 +44,7 @@ describe('mongodb', () => {
const mongoLibOverride = requireAgain(pathToMongoLib); const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions(); const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']); expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology', 'keepAlive', 'keepAliveInitialDelay']);
}); });
}); });
}); });

View File

@@ -2,7 +2,7 @@ import { model as User } from '../../../../../../website/server/models/user';
import amzLib from '../../../../../../website/server/libs/payments/amazon'; import amzLib from '../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments'; import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common'; 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'; import * as gems from '../../../../../../website/server/libs/payments/gems';
const { i18n } = common; const { i18n } = common;

View File

@@ -4,7 +4,7 @@ import nconf from 'nconf';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../website/server/models/user'; import { model as User } from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common'; 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'; import * as gems from '../../../../../../website/server/libs/payments/gems';
const BASE_URL = nconf.get('BASE_URL'); const BASE_URL = nconf.get('BASE_URL');

View File

@@ -1,4 +1,4 @@
import { apiError } from '../../../../../../website/server/libs/apiError'; import apiError from '../../../../../../website/server/libs/apiError';
import common from '../../../../../../website/common'; import common from '../../../../../../website/common';
import { import {
getOneTimePaymentInfo, getOneTimePaymentInfo,

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;
});
});

View File

@@ -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;
});
});
});

View File

@@ -6,7 +6,7 @@ import {
} from '../../../helpers/api-unit.helper'; } from '../../../helpers/api-unit.helper';
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight'; import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors'; 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', () => { describe('ensure access middlewares', () => {
let res; let req; let let res; let req; let

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -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;
});
});

View File

@@ -6,7 +6,7 @@ import {
generateNext, generateNext,
} from '../../../helpers/api-unit.helper'; } from '../../../helpers/api-unit.helper';
import { Forbidden } from '../../../../website/server/libs/errors'; import { Forbidden } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError'; import apiError from '../../../../website/server/libs/apiError';
function checkErrorThrown (next) { function checkErrorThrown (next) {
expect(next).to.have.been.calledOnce; expect(next).to.have.been.calledOnce;

View File

@@ -7,7 +7,7 @@ import {
generateNext, generateNext,
} from '../../../helpers/api-unit.helper'; } from '../../../helpers/api-unit.helper';
import { TooManyRequests } from '../../../../website/server/libs/errors'; 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'; import logger from '../../../../website/server/libs/logger';
describe('rateLimiter middleware', () => { describe('rateLimiter middleware', () => {
@@ -54,7 +54,6 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => { it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next); 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 () => { 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_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
sandbox.stub(logger, 'error'); sandbox.stub(logger, 'error');
sandbox.stub(RateLimiterMemory.prototype, 'consume') sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.'))); .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'); 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 () => { it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
// call for 31 times // call for 31 times
@@ -179,7 +112,6 @@ describe('rateLimiter middleware', () => {
it('uses the user id if supplied or the ip address', async () => { it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1; req.ip = 1;
@@ -206,51 +138,4 @@ describe('rateLimiter middleware', () => {
'X-RateLimit-Reset': sinon.match(Date), 'X-RateLimit-Reset': sinon.match(Date),
}); });
}); });
it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/register';
req.ip = 1;
await attachRateLimiter(req, res, next);
req.headers['x-api-user'] = 'user-1';
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
// user id an ip are counted as separate sources
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 27, // 2 calls with user id
'X-RateLimit-Reset': sinon.match(Date),
});
req.headers['x-api-user'] = undefined;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 24, // 3 calls with only ip
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 10,
'X-RateLimit-Reset': sinon.match(Date),
});
});
}); });

View File

@@ -1,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;
});
});

View File

@@ -1362,8 +1362,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'updateMany'); sandbox.spy(User, 'updateMany');
}); });
it('formats message', async () => { it('formats message', () => {
const chatMessage = await party.sendChat({ const chatMessage = party.sendChat({
message: 'a _new_ message with *markdown*', message: 'a _new_ message with *markdown*',
user: { user: {
_id: 'user-id', _id: 'user-id',
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
expect(chat.user).to.eql('user name'); expect(chat.user).to.eql('user name');
}); });
it('formats message as system if no user is passed in', async () => { it('formats message as system if no user is passed in', () => {
const chat = await party.sendChat({ message: 'a system message' }); const chat = party.sendChat({ message: 'a system message' });
expect(chat.text).to.eql('a system message'); expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true); expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist; expect(chat.user).to.not.exist;
}); });
it('updates users about new messages in party', async () => { it('updates users about new messages in party', () => {
await party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ 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({ const group = new Group({
type: 'guild', type: 'guild',
}); });
await group.sendChat({ message: 'message' }); group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ 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 () => { it('does not send update to user that sent the message', () => {
await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } }); party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.updateMany).to.be.calledWithMatch({
@@ -1445,18 +1445,18 @@ 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; party.memberCount = 5001;
await party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.updateMany).to.not.be.called;
}); });
it('skips sending messages to the tavern', async () => { it('skips sending messages to the tavern', () => {
party._id = TAVERN_ID; party._id = TAVERN_ID;
await party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.updateMany).to.not.be.called;
}); });
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
await guild.save(); await guild.save();
const groupMessage = await guild.sendChat({ message: 'Test message.' }); const groupMessage = guild.sendChat({ message: 'Test message.' });
await groupMessage.save(); await groupMessage.save();
await sleep(); await sleep();

View File

@@ -1,7 +1,7 @@
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { model as Webhook } from '../../../../website/server/models/webhook'; import { model as Webhook } from '../../../../website/server/models/webhook';
import { BadRequest } from '../../../../website/server/libs/errors'; import { BadRequest } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError'; import apiError from '../../../../website/server/libs/apiError';
describe('Webhook Model', () => { describe('Webhook Model', () => {
context('Instance Methods', () => { context('Instance Methods', () => {

View File

@@ -7,7 +7,6 @@ import {
describe('POST /challenges/:challengeId/flag', () => { describe('POST /challenges/:challengeId/flag', () => {
let user; let user;
let challengeGroup;
let challenge; let challenge;
beforeEach(async () => { beforeEach(async () => {
@@ -21,7 +20,6 @@ describe('POST /challenges/:challengeId/flag', () => {
}); });
user = groupLeader; user = groupLeader;
challengeGroup = group;
challenge = await generateChallenge(user, group); challenge = await generateChallenge(user, group);
}); });
@@ -61,19 +59,4 @@ describe('POST /challenges/:challengeId/flag', () => {
message: t('messageChallengeFlagAlreadyReported'), 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'),
});
});
}); });

View File

@@ -331,71 +331,5 @@ describe('POST /challenges', () => {
expect(updatedChallenge.summary).to.eql(summary); 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);
});
}); });
}); });

View File

@@ -3,7 +3,6 @@ import {
createAndPopulateGroup, createAndPopulateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('GET /groups/:groupId/chat', () => { describe('GET /groups/:groupId/chat', () => {
let user; let user;
@@ -38,34 +37,4 @@ describe('GET /groups/:groupId/chat', () => {
}); });
}); });
}); });
context('public Guild', () => {
let group;
before(async () => {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
chat: [
'Hello',
'Welcome to the Guild',
],
}));
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
});
it('returns error if user attempts to fetch a sunset Guild', async () => {
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
});
}); });

View File

@@ -223,23 +223,4 @@ describe('POST /chat/:chatId/flag', () => {
expect(auMessageToCheck).to.not.exist; expect(auMessageToCheck).to.not.exist;
}); });
it('validates that the message belongs to the passed group', async () => {
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Another Guild',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
const message = await anotherUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
}); });

View File

@@ -1,10 +1,8 @@
import { find } from 'lodash'; import { find } from 'lodash';
import { import {
generateUser,
createAndPopulateGroup, createAndPopulateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /chat/:chatId/like', () => { describe('POST /chat/:chatId/like', () => {
let user; let user;
@@ -41,21 +39,19 @@ describe('POST /chat/:chatId/like', () => {
}); });
}); });
it('Likes a chat', async () => { it('Returns an error when user tries to like their own message', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`); await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
expect(likeResult.likes[user._id]).to.equal(true); code: 404,
error: 'NotFound',
const groupWithChatLikes = await user.get(`/groups/${groupWithChat._id}`); message: t('messageGroupChatLikeOwnMessage'),
});
const messageToCheck = find(groupWithChatLikes.chat, { id: message.message.id });
expect(messageToCheck.likes[user._id]).to.equal(true);
}); });
it('Allows to likes their own chat message', async () => { it('Likes a chat', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`); const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`);
@@ -81,49 +77,4 @@ describe('POST /chat/:chatId/like', () => {
const messageToCheck = find(groupWithoutChatLikes.chat, { id: message.message.id }); const messageToCheck = find(groupWithoutChatLikes.chat, { id: message.message.id });
expect(messageToCheck.likes[user._id]).to.equal(false); expect(messageToCheck.likes[user._id]).to.equal(false);
}); });
it('validates that the message belongs to the passed group', async () => {
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Another Guild',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('does not like a message if the user is not in the group', async () => {
const thirdUser = await generateUser();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
await expect(thirdUser.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not like a message that belongs to a sunset public group', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: groupWithChat._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
}); });

View File

@@ -22,38 +22,4 @@ describe('GET /content', () => {
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach'); expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText')); expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
}); });
it('does not filter content for regular requests', async () => {
const res = await requester().get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.have.nested.property('gear.tree');
});
it('filters content automatically for iOS requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
expect(res).to.have.nested.property('appearances.background.beach');
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content automatically for Android requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
expect(res).to.not.have.nested.property('appearances.background.beach');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content if the request specifies a filter', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.have.nested.property('gear.tree');
expect(res).to.not.have.nested.property('gear.flat');
});
it('filters content if the request contains invalid filters', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
expect(res).to.not.have.property('backgroundsFlat');
});
}); });

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
resetHabiticaDB, resetHabiticaDB,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
describe('GET /coupons/', () => { describe('GET /coupons/', () => {
let user; let user;

View File

@@ -4,7 +4,7 @@ import {
translate as t, translate as t,
resetHabiticaDB, resetHabiticaDB,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
describe('POST /coupons/generate/:event', () => { describe('POST /coupons/generate/:event', () => {
let user; let user;

View File

@@ -1,46 +0,0 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('GET /debug/time-travel-time', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
});
afterEach(() => {
nconfStub.restore();
});
it('returns the shifted time', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const result = await user.get('/debug/time-travel-time');
expect(result.time).to.exist;
await user.post('/debug/jump-time', { disable: true });
});
it('returns shifted when the user is not an admin', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const regularUser = await generateUser();
const result = await regularUser.get('/debug/time-travel-time');
expect(result.time).to.exist;
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.get('/debug/time-travel-time'))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});

View File

@@ -5,23 +5,16 @@ import {
describe('POST /debug/add-hourglass', () => { describe('POST /debug/add-hourglass', () => {
let userToGetHourGlass; let userToGetHourGlass;
let nconfStub;
before(async () => { before(async () => {
userToGetHourGlass = await generateUser(); userToGetHourGlass = await generateUser();
}); });
beforeEach(() => { after(() => {
nconfStub = sandbox.stub(nconf, 'get'); nconf.set('IS_PROD', false);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
}); });
it('adds Hourglass to the current user', async () => { it('adds Hourglass to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGetHourGlass.post('/debug/add-hourglass'); await userToGetHourGlass.post('/debug/add-hourglass');
const userWithHourGlass = await userToGetHourGlass.get('/user'); const userWithHourGlass = await userToGetHourGlass.get('/user');
@@ -30,7 +23,7 @@ describe('POST /debug/add-hourglass', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(userToGetHourGlass.post('/debug/add-hourglass')) await expect(userToGetHourGlass.post('/debug/add-hourglass'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,23 +5,16 @@ import {
describe('POST /debug/add-ten-gems', () => { describe('POST /debug/add-ten-gems', () => {
let userToGainTenGems; let userToGainTenGems;
let nconfStub;
before(async () => { before(async () => {
userToGainTenGems = await generateUser(); userToGainTenGems = await generateUser();
}); });
beforeEach(() => { after(() => {
nconfStub = sandbox.stub(nconf, 'get'); nconf.set('IS_PROD', false);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
}); });
it('adds ten gems to the current user', async () => { it('adds ten gems to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGainTenGems.post('/debug/add-ten-gems'); await userToGainTenGems.post('/debug/add-ten-gems');
const userWithTenGems = await userToGainTenGems.get('/user'); const userWithTenGems = await userToGainTenGems.get('/user');
@@ -30,7 +23,7 @@ describe('POST /debug/add-ten-gems', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(userToGainTenGems.post('/debug/add-ten-gems')) await expect(userToGainTenGems.post('/debug/add-ten-gems'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

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

View File

@@ -1,86 +0,0 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/jump-time', () => {
let user;
let today;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
today = new Date();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
});
afterEach(() => {
nconfStub.restore();
});
after(async () => {
nconf.set('TIME_TRAVEL_ENABLED', true);
await user.post('/debug/jump-time', { disable: true });
nconf.set('TIME_TRAVEL_ENABLED', false);
});
it('Jumps forward', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
const tomorrow = new Date(today.valueOf());
tomorrow.setDate(today.getDate() + 1);
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
});
it('jumps back', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
const yesterday = new Date(today.valueOf());
yesterday.setDate(today.getDate() - 1);
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
});
it('can jump a lot', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
});
it('returns error when the user is not an admin', async () => {
const regularUser = await generateUser();
await expect(regularUser.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 400,
error: 'BadRequest',
message: 'You do not have permission to time travel.',
});
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});

View File

@@ -5,23 +5,16 @@ import {
describe('POST /debug/make-admin', () => { describe('POST /debug/make-admin', () => {
let user; let user;
let nconfStub;
before(async () => { before(async () => {
user = await generateUser(); user = await generateUser();
}); });
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => { afterEach(() => {
nconfStub.restore(); nconf.set('IS_PROD', false);
}); });
it('makes user an admin', async () => { it('makes user an admin', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await user.post('/debug/make-admin'); await user.post('/debug/make-admin');
await user.sync(); await user.sync();
@@ -30,7 +23,7 @@ describe('POST /debug/make-admin', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(user.post('/debug/make-admin')) await expect(user.post('/debug/make-admin'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

@@ -8,7 +8,6 @@ import {
describe('POST /debug/modify-inventory', () => { describe('POST /debug/modify-inventory', () => {
let user; let let user; let
originalItems; originalItems;
let nconfStub;
before(async () => { before(async () => {
originalItems = { originalItems = {
@@ -40,14 +39,8 @@ describe('POST /debug/modify-inventory', () => {
}); });
}); });
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => { afterEach(() => {
nconfStub.restore(); nconf.set('IS_PROD', false);
}); });
it('sets equipment', async () => { it('sets equipment', async () => {
@@ -156,7 +149,7 @@ describe('POST /debug/modify-inventory', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(user.post('/debug/modify-inventory')) await expect(user.post('/debug/modify-inventory'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,20 +5,13 @@ import {
describe('POST /debug/quest-progress', () => { describe('POST /debug/quest-progress', () => {
let user; let user;
let nconfStub;
beforeEach(async () => { beforeEach(async () => {
user = await generateUser(); user = await generateUser();
}); });
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => { afterEach(() => {
nconfStub.restore(); nconf.set('IS_PROD', false);
}); });
it('errors if user is not on a quest', async () => { it('errors if user is not on a quest', async () => {
@@ -55,7 +48,7 @@ describe('POST /debug/quest-progress', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(user.post('/debug/quest-progress')) await expect(user.post('/debug/quest-progress'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,20 +5,13 @@ import {
describe('POST /debug/set-cron', () => { describe('POST /debug/set-cron', () => {
let user; let user;
let nconfStub;
before(async () => { before(async () => {
user = await generateUser(); user = await generateUser();
}); });
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => { afterEach(() => {
nconfStub.restore(); nconf.set('IS_PROD', false);
}); });
it('sets last cron', async () => { it('sets last cron', async () => {
@@ -34,7 +27,7 @@ describe('POST /debug/set-cron', () => {
}); });
it('returns error when not in production mode', async () => { it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false); nconf.set('IS_PROD', true);
await expect(user.post('/debug/set-cron')) await expect(user.post('/debug/set-cron'))
.eventually.be.rejected.and.to.deep.equal({ .eventually.be.rejected.and.to.deep.equal({

View File

@@ -145,18 +145,6 @@ describe('POST /group', () => {
expect(updatedUser.party._id).to.eql(party._id); expect(updatedUser.party._id).to.eql(party._id);
}); });
it('removes seeking from user', async () => {
await user.updateOne({ 'party.seeking': new Date() });
await user.post('/groups', {
name: partyName,
type: partyType,
});
const updatedUser = await user.get('/user');
expect(updatedUser.party.seeking).to.not.exist;
});
it('does not award Party Up achievement to solo partier', async () => { it('does not award Party Up achievement to solo partier', async () => {
await user.post('/groups', { await user.post('/groups', {
name: partyName, name: partyName,

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import {
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
describe('GET /heroes/party/:groupId', () => { describe('GET /heroes/party/:groupId', () => {
let user; // admin user let user; // admin user

View File

@@ -10,8 +10,7 @@ describe('PUT /heroes/:heroId', () => {
const heroFields = [ const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron', '_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements', 'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions',
'stats',
]; ];
before(async () => { before(async () => {
@@ -252,159 +251,4 @@ describe('PUT /heroes/:heroId', () => {
expect(updatedHero.apiToken).to.not.equal(originalToken); expect(updatedHero.apiToken).to.not.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist; expect(updatedHero.apiTokenObscured).to.not.exist;
}); });
it('updates purchased hair customization', async () => {
const hero = await generateUser();
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
purchasedPath: 'purchased.hair.bangs.1',
purchasedVal: true,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.purchased.hair.bangs['1']).to.equal(true);
// test hero values
await hero.sync();
expect(hero.purchased.hair.bangs['1']).to.equal(true);
});
it('updates purchased customization', async () => {
const hero = await generateUser();
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
purchasedPath: 'purchased.background.beach',
purchasedVal: true,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.purchased.background.beach).to.equal(true);
// test hero values
await hero.sync();
expect(hero.purchased.background.beach).to.equal(true);
});
it('updates giving nested achievement', async () => {
const hero = await generateUser();
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.quests.dilatory',
achievementVal: 2,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.quests.dilatory).to.equal(2);
// test hero values
await hero.sync();
expect(hero.achievements.quests.dilatory).to.equal(2);
});
it('updates taking away nested achievement', async () => {
const hero = await generateUser({ 'achievements.quests.dilatory': 3 });
expect(hero.achievements.quests.dilatory).to.equal(3);
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.quests.dilatory',
achievementVal: 0,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.quests.dilatory).to.equal(0);
// test hero values
await hero.sync();
expect(hero.achievements.quests.dilatory).to.equal(0);
});
it('updates giving achievement', async () => {
const hero = await generateUser();
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.partyOn',
achievementVal: true,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.partyOn).to.equal(true);
// test hero values
await hero.sync();
expect(hero.achievements.partyOn).to.equal(true);
});
it('updates taking away achievement', async () => {
const hero = await generateUser({ 'achievements.partyUp': true });
expect(hero.achievements.partyUp).to.equal(true);
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.partyUp',
achievementVal: false,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.partyUp).to.equal(false);
// test hero values
await hero.sync();
expect(hero.achievements.partyUp).to.equal(false);
});
it('updates giving numbered achievement', async () => {
const hero = await generateUser();
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.streak',
achievementVal: 42,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.streak).to.equal(42);
// test hero values
await hero.sync();
expect(hero.achievements.streak).to.equal(42);
});
it('updates setting numbered achievement to 0', async () => {
const hero = await generateUser({ 'achievements.streak': 42 });
expect(hero.achievements.streak).to.equal(42);
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
achievementPath: 'achievements.streak',
achievementVal: 0,
});
// test response
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.achievements.streak).to.equal(0);
// test hero values
await hero.sync();
expect(hero.achievements.streak).to.equal(0);
});
}); });

View File

@@ -2,7 +2,7 @@ import {
generateUser, generateUser,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import { apiError } from '../../../../../../website/server/libs/apiError'; import apiError from '../../../../../../website/server/libs/apiError';
describe('payments : paypal #checkoutSuccess', () => { describe('payments : paypal #checkoutSuccess', () => {
const endpoint = '/paypal/checkout/success'; const endpoint = '/paypal/checkout/success';

View File

@@ -3,7 +3,7 @@ import {
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import shared from '../../../../../../website/common'; import shared from '../../../../../../website/common';
import { apiError } from '../../../../../../website/server/libs/apiError'; import apiError from '../../../../../../website/server/libs/apiError';
describe('payments : paypal #subscribe', () => { describe('payments : paypal #subscribe', () => {
const endpoint = '/paypal/subscribe'; const endpoint = '/paypal/subscribe';

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import { apiError } from '../../../../../../website/server/libs/apiError'; import apiError from '../../../../../../website/server/libs/apiError';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeSuccess', () => { describe('payments : paypal #subscribeSuccess', () => {

View File

@@ -7,7 +7,7 @@ import {
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { quests as questScrolls } from '../../../../../website/common/script/content/quests'; import { quests as questScrolls } from '../../../../../website/common/script/content/quests';
import { chatModel as Chat } from '../../../../../website/server/models/message'; import { chatModel as Chat } from '../../../../../website/server/models/message';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => { describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup; let questingGroup;

View File

@@ -17,5 +17,9 @@ describe('GET /shops/backgrounds', () => {
expect(shop.notes).to.eql(t('backgroundShop')); expect(shop.notes).to.eql(t('backgroundShop'));
expect(shop.imageName).to.equal('background_shop'); expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array'); expect(shop.sets).to.be.an('array');
const sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
}); });
}); });

View File

@@ -5,15 +5,9 @@ import {
describe('GET /shops/time-travelers', () => { describe('GET /shops/time-travelers', () => {
let user; let user;
let clock;
beforeEach(async () => { beforeEach(async () => {
user = await generateUser(); user = await generateUser();
clock = sinon.useFakeTimers(new Date('2024-06-08'));
});
afterEach(() => {
clock.restore();
}); });
it('returns a valid shop object', async () => { it('returns a valid shop object', async () => {

View File

@@ -1,5 +1,5 @@
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
import { import {
generateUser, generateUser,
sleep, sleep,

View File

@@ -8,7 +8,7 @@ import {
generateChallenge, generateChallenge,
sleep, sleep,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { apiError } from '../../../../../website/server/libs/apiError'; import apiError from '../../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => { describe('POST /user/class/cast/:spellId', () => {
let user; let user;

View File

@@ -33,20 +33,6 @@ describe('POST /user/purchase/:type/:key', () => {
expect(user.items[type][key]).to.equal(1); expect(user.items[type][key]).to.equal(1);
}); });
it('purchases animal ears', async () => {
await user.post('/user/purchase/gear/headAccessory_special_tigerEars');
await user.sync();
expect(user.items.gear.owned.headAccessory_special_tigerEars).to.equal(true);
});
it('purchases animal tails', async () => {
await user.post('/user/purchase/gear/back_special_pandaTail');
await user.sync();
expect(user.items.gear.owned.back_special_pandaTail).to.equal(true);
});
it('can convert gold to gems if subscribed', async () => { it('can convert gold to gems if subscribed', async () => {
const oldBalance = user.balance; const oldBalance = user.balance;
await user.updateOne({ await user.updateOne({

View File

@@ -5,7 +5,7 @@ import {
describe('POST /user/unlock', () => { describe('POST /user/unlock', () => {
let user; let user;
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie'; const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars'; const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
const unlockCost = 1.25; const unlockCost = 1.25;
const usersStartingGems = 5; const usersStartingGems = 5;

View File

@@ -274,14 +274,6 @@ describe('PUT /user', () => {
expect(get(updatedUser.preferences, type)).to.eql(item); expect(get(updatedUser.preferences, type)).to.eql(item);
}); });
}); });
it('updates user when background is unequipped', async () => {
expect(get(user.preferences, 'background')).to.not.eql('');
const updatedUser = await user.put('/user', { 'preferences.background': '' });
expect(get(updatedUser.preferences, 'background')).to.eql('');
});
}); });
context('Improvement Categories', () => { context('Improvement Categories', () => {

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