Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3489b88752 | |||
| 94bda30385 | |||
| e8bbdc2cb8 | |||
| d465efaf96 | |||
| 3a08de7ab3 | |||
| e6ffd69148 | |||
| 746fcfff49 | |||
| 8aa343d390 | |||
| d80c43c82a | |||
| 80e4b8617a | |||
| b3fac011a9 | |||
| 8f4d871911 | |||
| 19373ce84d | |||
| 55bfca20d9 | |||
| 2ee2b05d1c | |||
| 3d3db1bdd9 | |||
| a5dff99fa1 | |||
| 25435218e1 | |||
| 7746049bb4 | |||
| 036bf43cf3 | |||
| c4e0127a37 | |||
| 7ae059e243 | |||
| 81b9a0b92d | |||
| a6e87452a6 | |||
| c40d384913 | |||
| 65144aef28 | |||
| cc7683a871 | |||
| 31b2781333 | |||
| d37d3bc5ac | |||
| ef3a28791e | |||
| c3c2607bca | |||
| 7a6d64f158 | |||
| 836e63246d | |||
| 61585b2549 | |||
| 07275bd522 | |||
| 74fc543ef2 | |||
| 8c90e5472b | |||
| c8d9ba6c8e | |||
| 1675c2749b | |||
| e85a2bae14 | |||
| 3355500fba | |||
| 486f15df0f | |||
| f0b6b5611c | |||
| 7e45c79714 | |||
| 8da6065355 | |||
| a212363bda | |||
| 2e19e73b9e | |||
| 1047b0e03b | |||
| 159f850bd1 | |||
| 42083efb7e | |||
| f21e800b0b | |||
| 40122e5621 | |||
| 0ae19d9107 | |||
| 68bfebcf30 | |||
| 3e93911e70 | |||
| 4ea8636f03 | |||
| 9f97a09b8c | |||
| eccc115b73 | |||
| 2b26eb2bd1 | |||
| 8e042cabc4 | |||
| 8abe167848 | |||
| 3414f962e2 | |||
| 1b68e6d4d3 | |||
| 5dd9711413 | |||
| a542277a41 | |||
| cdf8556fd6 | |||
| 3d93390a7a | |||
| 59f9cfa0f4 | |||
| 80d7804f69 | |||
| 4e5efe09a3 | |||
| d42a597672 | |||
| ea17b2e9c7 | |||
| f56708cd88 | |||
| 005d14f6e8 | |||
| c05a96ce6c | |||
| 8fdbfb9dc6 | |||
| 057a642baa | |||
| 6c522157a7 | |||
| ba9a1ab2a9 | |||
| 4767461c4f | |||
| 847c97dc8f | |||
| 215b26acac | |||
| e223e7821a | |||
| 8134fa7c00 | |||
| 84208f612e | |||
| 57e06334c0 | |||
| be695d25b3 | |||
| 77ee83f467 | |||
| 86556e346b | |||
| 2007a872c6 | |||
| a8348038de | |||
| 87bcd69979 | |||
| 8f8e84d0c7 | |||
| 2c18cb00cc | |||
| daa0fd18c0 | |||
| 5c555cbf88 | |||
| 7379c7b230 | |||
| c055537c38 | |||
| 7559feec8e | |||
| 43808696a8 | |||
| 72fb41c7e0 | |||
| 3bf18e09ed | |||
| 407a901883 | |||
| 81a008906b | |||
| 992a978923 | |||
| a8062ad615 | |||
| 781a904583 | |||
| d87946d912 | |||
| 7456ff2def | |||
| e0af620b40 | |||
| bb295551b5 | |||
| fce400f323 | |||
| c0ffb8b968 | |||
| 72539f9ba3 | |||
| dabd466719 | |||
| 8bf2304330 | |||
| 6937dc4e4e | |||
| 2917955ef0 | |||
| 55d13e44d4 | |||
| 90096f995f | |||
| 5c74c2b914 | |||
| 1f1a44e16f | |||
| a275109a3e | |||
| c65457690b | |||
| f740f12b97 | |||
| 9fd0bfae46 | |||
| bee23efbef | |||
| a504b18ce4 | |||
| f556b102c6 | |||
| ac62de7bd8 | |||
| 5ff3cc35a6 | |||
| 215e5e1c40 | |||
| 02ca96ea51 | |||
| e70ae4e9aa | |||
| e2bf8ae493 | |||
| 931a70a797 | |||
| e2d2a05315 | |||
| be041f734d | |||
| c430d2279c | |||
| ef592cf35f | |||
| f24cd10a79 | |||
| 2cd4e45016 | |||
| 8aaff7ae23 | |||
| 69a9fb89ef | |||
| e8eeb76cab | |||
| 2029739a1b | |||
| 5cef106ea5 | |||
| e096d7ac42 | |||
| 6db998e726 | |||
| 29c658b042 | |||
| 66710b8f38 | |||
| c77db3d625 | |||
| c947fa97d9 | |||
| b2b9702797 | |||
| e92503f032 | |||
| 8faa5b0582 | |||
| 95494c685b | |||
| 10978d46ab | |||
| 447eb6a0c4 | |||
| 3dec49b72c | |||
| 472d03f276 | |||
| fd9a27c3ab | |||
| a5c1423837 | |||
| e9829b8b60 | |||
| 7ecb83dc7e | |||
| e8ffe2286c | |||
| fe63436a57 | |||
| 5b93b9b37a | |||
| 1d55027791 | |||
| 83f0984da1 | |||
| 53d4f75cab | |||
| da45eb2adf | |||
| 3bf4af8d8b | |||
| f030691fac | |||
| 1f94e51693 | |||
| 86e7d7a72b | |||
| 140b852e03 | |||
| 8f949ce1cc | |||
| 5e21285370 | |||
| 7a65bc2d8d | |||
| a32fadbcbd | |||
| 305192ed1f | |||
| 7644e202c9 | |||
| d11c8442ef | |||
| d8b5391425 | |||
| dd287cd719 | |||
| e809d1f6e4 | |||
| da90fa6aaf | |||
| 77392db25a | |||
| 1bc1bf0621 | |||
| 635a258d62 | |||
| 384fb505c1 | |||
| 3e0bc36373 | |||
| 0a431afaaf | |||
| 8c911bcd41 | |||
| dcb7ac5955 | |||
| fb730942a0 | |||
| 9c92bf73f5 | |||
| 58f195fdb7 | |||
| 4b86c9c8a7 | |||
| 4cc689ec63 | |||
| 8690484f5e | |||
| 1f3e5b7a76 | |||
| 61c790f291 | |||
| b3440fa3a8 | |||
| a3f1835d1d | |||
| 9226f6f70e | |||
| 1130f9957f | |||
| ad1fd03aad | |||
| 6c93033ad2 | |||
| dd97b11b60 | |||
| 59ba07d4f3 | |||
| d2bfd1e3a9 | |||
| a8264bf526 | |||
| f202f2b3d3 | |||
| 4ea9f8282e | |||
| 205d84a111 | |||
| 5810853cc2 | |||
| 4547204bd8 | |||
| f17a0c91a3 | |||
| 16e1523b08 | |||
| 0f06ec1ab8 | |||
| 641266122a | |||
| 5ba939ee9c | |||
| c979e568f1 | |||
| 93f0c240f9 | |||
| ad04b077a4 | |||
| 7ffc454320 | |||
| dae0fbff16 | |||
| 5648092112 | |||
| 275b15b773 | |||
| 1025635e34 | |||
| 836cbdb81e | |||
| be922de7ba | |||
| 3a2f5e724d | |||
| 8a105c6a14 | |||
| 7f1c64a50e | |||
| 125f472f34 | |||
| bafd273475 | |||
| 365cb1c2eb |
@@ -82,7 +82,7 @@ jobs:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:sanity
|
||||
|
||||
|
||||
common:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -129,13 +129,13 @@ jobs:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:content
|
||||
|
||||
|
||||
api-unit:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -144,11 +144,13 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -158,15 +160,17 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api:unit
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
api-v3-integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -176,10 +180,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -189,15 +194,18 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api-v3:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
api-v4-integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -207,10 +215,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -220,6 +229,7 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api-v4:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data*
|
||||
/mongodb-*
|
||||
/.nyc_output
|
||||
|
||||
@@ -8,18 +8,26 @@
|
||||
"AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
|
||||
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
|
||||
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"BASE_URL": "http://localhost:3000",
|
||||
"BLOCKED_IPS": "",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"CRON_SAFE_MODE": "false",
|
||||
"CRON_SEMI_SAFE_MODE": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"DISABLE_REQUEST_LOGGING": "true",
|
||||
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
||||
"EMAIL_SERVER_AUTH_PASSWORD": "password",
|
||||
"EMAIL_SERVER_AUTH_USER": "user",
|
||||
"EMAIL_SERVER_URL": "http://example.com",
|
||||
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
||||
"ENABLE_CONSOLE_LOGS_IN_PROD": "false",
|
||||
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"FACEBOOK_KEY": "123456789012345",
|
||||
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
|
||||
@@ -29,15 +37,16 @@
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"IGNORE_REDIRECT": "true",
|
||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs&directConnection=true&readPreference=secondary",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
@@ -55,44 +64,33 @@
|
||||
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"PORT": 3000,
|
||||
"PUSH_CONFIGS_APN_ENABLED": "false",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
|
||||
"PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PASSWORD": "12345678",
|
||||
"REDIS_PORT": "1234",
|
||||
"S3_ACCESS_KEY_ID": "accessKeyId",
|
||||
"S3_BUCKET": "bucket",
|
||||
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
||||
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||
"SLOW_REQUEST_THRESHOLD": 1000,
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"BLOCKED_IPS": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PORT": "1234",
|
||||
"REDIS_PASSWORD": "12345678",
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs&directConnection=true&readPreference=secondary",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"SLOW_REQUEST_THRESHOLD": 1000
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"WEB_CONCURRENCY": 1
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
services:
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
- /usr/src/habitica/website/client/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
image: mongo:5.0.23
|
||||
restart: unless-stopped
|
||||
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,24 @@
|
||||
networks:
|
||||
mongodb-network:
|
||||
name: "mongodb-network"
|
||||
driver: bridge
|
||||
services:
|
||||
mongodb:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb-only"
|
||||
networks:
|
||||
- mongodb-network
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
networks:
|
||||
mongodb-network:
|
||||
name: "mongodb-network"
|
||||
driver: bridge
|
||||
services:
|
||||
mongodb:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb-test"
|
||||
networks:
|
||||
- mongodb-network
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker-testing:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
@@ -1,35 +1,56 @@
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
build: .
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: ["npm", "run", "client:dev"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev:docker"]
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
server:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
- /usr/src/habitica/website/client/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb"
|
||||
networks:
|
||||
- habitica
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
networks:
|
||||
habitica:
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from 'path';
|
||||
import babel from 'gulp-babel';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import spawn from 'cross-spawn';
|
||||
import clean from 'rimraf';
|
||||
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
@@ -35,7 +35,7 @@ gulp.task('build:prod', gulp.series(
|
||||
// When used on windows `run-rs` must first be run without the `--keep` option
|
||||
// in order to be setup correctly, afterwards it can be used.
|
||||
|
||||
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
|
||||
const MONGO_PATH = path.join(__dirname, '/../mongodb-data-docker/');
|
||||
|
||||
gulp.task('build:prepare-mongo', async () => {
|
||||
if (fs.existsSync(MONGO_PATH)) {
|
||||
@@ -51,29 +51,32 @@ gulp.task('build:prepare-mongo', async () => {
|
||||
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
|
||||
|
||||
// use run-rs without --keep, kill it as soon as the replica set starts
|
||||
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
|
||||
const dockerMongoProcess = spawn('npm', ['run', 'docker:mongo:dev']);
|
||||
|
||||
for await (const chunk of runRsProcess.stdout) {
|
||||
let manuallyStopped = false;
|
||||
|
||||
for await (const chunk of dockerMongoProcess.stdout) {
|
||||
const stringChunk = chunk.toString();
|
||||
console.log(stringChunk); // eslint-disable-line no-console
|
||||
// kills the process after the replica set is setup
|
||||
if (stringChunk.includes('Started replica set')) {
|
||||
if (stringChunk.includes('mongod startup complete')) {
|
||||
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
|
||||
runRsProcess.kill();
|
||||
dockerMongoProcess.kill();
|
||||
manuallyStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
let error = '';
|
||||
for await (const chunk of runRsProcess.stderr) {
|
||||
for await (const chunk of dockerMongoProcess.stderr) {
|
||||
const stringChunk = chunk.toString();
|
||||
error += stringChunk;
|
||||
}
|
||||
|
||||
const exitCode = await new Promise(resolve => {
|
||||
runRsProcess.on('close', resolve);
|
||||
dockerMongoProcess.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode || error.length > 0) {
|
||||
if (!manuallyStopped && (exitCode || error.length > 0)) {
|
||||
// remove any leftover files
|
||||
clean.sync(MONGO_PATH);
|
||||
|
||||
|
||||
@@ -6,9 +6,21 @@ gulp.task('cache:content', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const {
|
||||
CONTENT_CACHE_PATH,
|
||||
getLocalizedContentResponse,
|
||||
IOS_FILTER,
|
||||
ANDROID_FILTER,
|
||||
buildFilterObject,
|
||||
hashForFilter,
|
||||
} = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
const iosHash = hashForFilter(IOS_FILTER);
|
||||
const iosFilterObj = buildFilterObject(IOS_FILTER);
|
||||
const androidHash = hashForFilter(ANDROID_FILTER);
|
||||
const androidFilterObj = buildFilterObject(ANDROID_FILTER);
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
@@ -26,33 +38,56 @@ gulp.task('cache:content', done => {
|
||||
getLocalizedContentResponse(langCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('cache:i18n', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { BROWSER_SCRIPT_CACHE_PATH, geti18nBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
fs.mkdirSync(BROWSER_SCRIPT_CACHE_PATH);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
|
||||
// create and save the i18n browser script for each language
|
||||
langCodes.forEach(languageCode => {
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}${languageCode}.js`,
|
||||
geti18nBrowserScript(languageCode),
|
||||
`${CONTENT_CACHE_PATH}${langCode}${iosHash}.json`,
|
||||
getLocalizedContentResponse(langCode, iosFilterObj),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
`${CONTENT_CACHE_PATH}${langCode}${androidHash}.json`,
|
||||
getLocalizedContentResponse(langCode, androidFilterObj),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
function safeMkdir (path) {
|
||||
try {
|
||||
fs.mkdirSync(path);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('cache:i18n', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { BROWSER_SCRIPT_CACHE_PATH, geti18nCoreBrowserScript, geti18nContentBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folders (if they doesn't exist)
|
||||
safeMkdir(BROWSER_SCRIPT_CACHE_PATH);
|
||||
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}core/`);
|
||||
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}content/`);
|
||||
|
||||
// create and save the i18n browser script for each language
|
||||
langCodes.forEach(languageCode => {
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}core/${languageCode}.js`,
|
||||
geti18nCoreBrowserScript(languageCode),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}content/${languageCode}.js`,
|
||||
geti18nContentBrowserScript(languageCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,6 +53,11 @@ gulp.task('test:prepare:mongo', cb => {
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||
|
||||
console.info({
|
||||
mongooseOptions,
|
||||
connectionUrl,
|
||||
});
|
||||
|
||||
mongoose.connect(connectionUrl, mongooseOptions)
|
||||
.then(() => mongoose.connection.dropDatabase())
|
||||
.then(() => mongoose.connection.close()).then(() => {
|
||||
|
||||
@@ -10,7 +10,7 @@ function setUpServer () {
|
||||
|
||||
setupNconf();
|
||||
|
||||
// We require src/server and npt src/index because
|
||||
// We require src/server and not src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
require('../website/server/server'); // eslint-disable-line global-require
|
||||
|
||||
@@ -11,7 +11,7 @@ const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* Award users every extant pet and mount
|
||||
* Award every extant piece of equippable gear
|
||||
*/
|
||||
|
||||
async function updateUser (user) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20181203_take_this';
|
||||
const MIGRATION_NAME = 'YYYYMMDD_take_this';
|
||||
const CHALLENGE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
@@ -41,15 +42,15 @@ async function updateUser (user) {
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (push) {
|
||||
return User.update({ _id: user._id }, { $set: set, $push: push }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set, $push: push }).exec();
|
||||
}
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
challenges: '00708425-d477-41a5-bf27-6270466e7976',
|
||||
challenges: CHALLENGE_ID,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
@@ -72,7 +73,7 @@ export default async function processUsers () {
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.38.2",
|
||||
"version": "5.47.5",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,8 +19,8 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.6.5",
|
||||
"cwait": "^1.1.1",
|
||||
@@ -39,7 +39,8 @@
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"habitica-markdown": "^4.1.0",
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
@@ -48,13 +49,15 @@
|
||||
"lodash": "^4.17.21",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"mongoose": "^8.23.0",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
"on-headers": "^1.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"passport": "^0.5.3",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
@@ -70,11 +73,9 @@
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
@@ -101,13 +102,16 @@
|
||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:dev:docker": "cd website/client && npm run serve:docker",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "node --watch ./website/server/index.js",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "node --watch --inspect ./website/server/index.js",
|
||||
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"docker:aio": "docker compose -f docker-compose.yml up",
|
||||
"docker:mongo:dev": "docker compose -f docker-compose.mongo-only.yml up",
|
||||
"docker:mongo:dev:down": "docker compose -f docker-compose.mongo-only.yml down",
|
||||
"docker:mongo:test": "docker compose -f docker-compose.mongo-test-local.yml up",
|
||||
"mongo:test": "node scripts/start-local-mongo.mjs --test-db",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
@@ -123,7 +127,6 @@
|
||||
"monk": "^7.3.4",
|
||||
"nyc": "^15.1.0",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -1,595 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import { Visitor } from 'universal-analytics';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
|
||||
sandbox.stub(Visitor.prototype, 'event');
|
||||
sandbox.stub(Visitor.prototype, 'transaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#getServiceByEnvironment', () => {
|
||||
it('returns mock methods when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment())
|
||||
.to.equal(analyticsService.mockAnalyticsService);
|
||||
});
|
||||
|
||||
it('returns real methods when in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
|
||||
.to.equal(analyticsService.track);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
|
||||
.to.equal(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType; let
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', () => {
|
||||
data.itemKey = 'headAccessory_special_foxEars';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Fox Ears',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', () => {
|
||||
data.itemKey = 'Wolf';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Wolf Egg',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', () => {
|
||||
data.itemKey = 'Cake_Skeleton';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Bare Bones Cake',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', () => {
|
||||
data.itemKey = 'Golden';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Golden Hatching Potion',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', () => {
|
||||
data.itemKey = 'atom1';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', () => {
|
||||
data.itemKey = 'seafoam';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Seafoam',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'Cron',
|
||||
ec: 'behavior',
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data; let
|
||||
itemSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
itemSpy = sandbox.stub().returnsThis();
|
||||
|
||||
Visitor.prototype.transaction.returns({
|
||||
item: itemSpy,
|
||||
send: sandbox.stub().yields(),
|
||||
});
|
||||
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
paymentMethod: 'PayPal',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1,
|
||||
sku: 'paypal-checkout',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
expect(Visitor.prototype.transaction).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'checkout',
|
||||
ec: 'commerce',
|
||||
el: 'PayPal',
|
||||
ev: 8,
|
||||
});
|
||||
expect(Visitor.prototype.transaction).to.be.calledWith('user-id', 8);
|
||||
expect(itemSpy).to.be.calledWith(8, 1, 'paypal-checkout', 'Gems', 'checkout');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
it('has stubbed track method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
|
||||
});
|
||||
|
||||
it('has stubbed trackPurchase method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,7 @@ describe('bug-report', () => {
|
||||
emailData: {
|
||||
BROWSER_UA: userAgent,
|
||||
REPORT_MSG: userMessage,
|
||||
USER_ANALYTICS: undefined,
|
||||
USER_CLASS: 'warrior',
|
||||
USER_CONSECUTIVE_MONTHS: 0,
|
||||
USER_COSTUME: 'false',
|
||||
|
||||
@@ -13,7 +13,6 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
@@ -41,20 +40,17 @@ describe('cron', async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.spy(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (clock !== null) clock.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
@@ -63,7 +59,7 @@ describe('cron', async () => {
|
||||
it('resets user.items.lastDrop.count', async () => {
|
||||
user.items.lastDrop.count = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
});
|
||||
@@ -71,26 +67,11 @@ describe('cron', async () => {
|
||||
it('increments user cron count', async () => {
|
||||
const cronCountBefore = user.flags.cronCount;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||
});
|
||||
|
||||
it('calls analytics', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', async () => {
|
||||
beforeEach(async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -101,7 +82,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -112,7 +93,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -122,7 +103,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -131,7 +112,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
user.purchased.plan.dateUpdated = undefined;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -142,7 +123,7 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
@@ -150,7 +131,7 @@ describe('cron', async () => {
|
||||
it('resets plan.dateUpdated on a new month', async () => {
|
||||
const currentMonth = moment().startOf('month');
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
@@ -158,7 +139,7 @@ describe('cron', async () => {
|
||||
it('increments plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
@@ -166,7 +147,7 @@ describe('cron', async () => {
|
||||
it('increments plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
@@ -175,7 +156,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
@@ -184,7 +165,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
||||
});
|
||||
@@ -196,7 +177,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
@@ -206,7 +187,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -214,7 +195,7 @@ describe('cron', async () => {
|
||||
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
@@ -225,7 +206,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
@@ -264,7 +245,7 @@ describe('cron', async () => {
|
||||
// Add 2 days so that we're sure we're not affected by any start-of-month effects
|
||||
// e.g., from time zone oddness.
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
user: user1, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -276,7 +257,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
user: user1, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -311,7 +292,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
user: user3, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -323,7 +304,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
user: user3, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -358,7 +339,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
user: user6, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user6.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -391,7 +372,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
user: user12, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -403,7 +384,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
user: user12, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -439,7 +420,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
@@ -452,7 +433,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
});
|
||||
// subscription has been erased by now
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
||||
@@ -471,7 +452,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -482,14 +463,14 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
||||
});
|
||||
@@ -497,7 +478,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||
});
|
||||
@@ -505,7 +486,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
||||
});
|
||||
@@ -513,7 +494,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
||||
});
|
||||
@@ -521,7 +502,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
});
|
||||
@@ -530,7 +511,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -538,7 +519,7 @@ describe('cron', async () => {
|
||||
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
});
|
||||
@@ -564,7 +545,7 @@ describe('cron', async () => {
|
||||
it('should make uncompleted todos redder', async () => {
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||
});
|
||||
@@ -573,7 +554,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
||||
});
|
||||
@@ -582,7 +563,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
@@ -608,7 +589,7 @@ describe('cron', async () => {
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
@@ -635,7 +616,7 @@ describe('cron', async () => {
|
||||
const original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
let listsAreEqual = true;
|
||||
@@ -675,7 +656,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
@@ -686,7 +667,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
@@ -696,14 +677,14 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
});
|
||||
@@ -711,7 +692,7 @@ describe('cron', async () => {
|
||||
it('should set tasks completed to false', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -720,7 +701,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -729,7 +710,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -739,7 +720,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -749,7 +730,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -759,7 +740,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
@@ -770,7 +751,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
@@ -784,7 +765,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -797,7 +778,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -808,7 +789,7 @@ describe('cron', async () => {
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -816,7 +797,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -829,7 +810,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(-1);
|
||||
@@ -841,7 +822,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
@@ -862,7 +843,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[1].frequency = 'daily';
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(48);
|
||||
@@ -886,7 +867,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -897,7 +878,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].up = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -909,7 +890,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.equal(1);
|
||||
@@ -928,7 +909,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -941,7 +922,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -955,7 +936,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -964,7 +945,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 8;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -988,7 +969,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1002,7 +983,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset after user CDS
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1026,7 +1007,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1036,7 +1017,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1060,7 +1041,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1084,7 +1065,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1098,7 +1079,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1107,7 +1088,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 32;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1132,7 +1113,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1156,7 +1137,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1166,7 +1147,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1199,7 +1180,7 @@ describe('cron', async () => {
|
||||
user.stats.lvl = 2;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.history.exp).to.have.lengthOf(1);
|
||||
@@ -1212,7 +1193,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(1);
|
||||
@@ -1224,7 +1205,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
@@ -1238,7 +1219,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1256,7 +1237,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1280,7 +1261,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1307,7 +1288,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1333,7 +1314,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1360,7 +1341,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1381,7 +1362,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1401,7 +1382,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1420,7 +1401,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
||||
|
||||
@@ -1436,7 +1417,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
@@ -1449,7 +1430,7 @@ describe('cron', async () => {
|
||||
user.stats.mp = 120;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
|
||||
|
||||
@@ -1482,7 +1463,7 @@ describe('cron', async () => {
|
||||
|
||||
it('resets user progress', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.party.quest.progress.up).to.equal(0);
|
||||
expect(user.party.quest.progress.down).to.equal(0);
|
||||
@@ -1491,7 +1472,7 @@ describe('cron', async () => {
|
||||
|
||||
it('applies the user progress', async () => {
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
@@ -1529,19 +1510,19 @@ describe('cron', async () => {
|
||||
describe('login incentives', async () => {
|
||||
it('increments incentive counter each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
});
|
||||
|
||||
it('pushes a notification of the day\'s incentive each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
@@ -1549,13 +1530,13 @@ describe('cron', async () => {
|
||||
|
||||
it('replaces previous notifications', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
||||
@@ -1566,7 +1547,7 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
|
||||
daysMissed = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
@@ -1574,14 +1555,14 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards user bard robes if login incentive is 1', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
||||
@@ -1591,7 +1572,7 @@ describe('cron', async () => {
|
||||
it('awards user incentive backgrounds if login incentive is 2', async () => {
|
||||
user.loginIncentives = 1;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
expect(user.purchased.background.blue).to.eql(true);
|
||||
@@ -1605,7 +1586,7 @@ describe('cron', async () => {
|
||||
it('awards user Bard Hat if login incentive is 3', async () => {
|
||||
user.loginIncentives = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(3);
|
||||
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
||||
@@ -1615,7 +1596,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
|
||||
user.loginIncentives = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(4);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1625,7 +1606,7 @@ describe('cron', async () => {
|
||||
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
|
||||
user.loginIncentives = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(5);
|
||||
|
||||
@@ -1639,7 +1620,7 @@ describe('cron', async () => {
|
||||
it('awards user moon quest if login incentive is 7', async () => {
|
||||
user.loginIncentives = 6;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(7);
|
||||
expect(user.items.quests.moon1).to.eql(1);
|
||||
@@ -1649,7 +1630,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
|
||||
user.loginIncentives = 9;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(10);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1659,7 +1640,7 @@ describe('cron', async () => {
|
||||
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
|
||||
user.loginIncentives = 13;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(14);
|
||||
|
||||
@@ -1673,7 +1654,7 @@ describe('cron', async () => {
|
||||
it('awards user a bard instrument if login incentive is 18', async () => {
|
||||
user.loginIncentives = 17;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(18);
|
||||
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
||||
@@ -1683,7 +1664,7 @@ describe('cron', async () => {
|
||||
it('awards user second moon quest if login incentive is 22', async () => {
|
||||
user.loginIncentives = 21;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(22);
|
||||
expect(user.items.quests.moon2).to.eql(1);
|
||||
@@ -1693,7 +1674,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
|
||||
user.loginIncentives = 25;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(26);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1703,7 +1684,7 @@ describe('cron', async () => {
|
||||
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
|
||||
user.loginIncentives = 29;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(30);
|
||||
|
||||
@@ -1718,7 +1699,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
|
||||
user.loginIncentives = 34;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(35);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1728,7 +1709,7 @@ describe('cron', async () => {
|
||||
it('awards user the third moon quest if login incentive is 40', async () => {
|
||||
user.loginIncentives = 39;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(40);
|
||||
expect(user.items.quests.moon3).to.eql(1);
|
||||
@@ -1738,7 +1719,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
|
||||
user.loginIncentives = 44;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(45);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1748,7 +1729,7 @@ describe('cron', async () => {
|
||||
it('awards user a saddle if login incentive is 50', async () => {
|
||||
user.loginIncentives = 49;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(50);
|
||||
expect(user.items.food.Saddle).to.eql(1);
|
||||
@@ -1766,7 +1747,6 @@ describe('cron wrapper', () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -234,7 +234,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -254,7 +254,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
|
||||
@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
|
||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||
});
|
||||
|
||||
it('highlights users with case-insensitive matching', async () => {
|
||||
const text = '@USER: message @User2 @USER3';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
|
||||
});
|
||||
|
||||
it('doesn\'t highlight nonexisting users', async () => {
|
||||
const text = '@nouser message';
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
|
||||
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('localAnalytics', () => {
|
||||
let user;
|
||||
let localAnalytics;
|
||||
before(() => {
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
|
||||
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
|
||||
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
email: 'email@example.com',
|
||||
},
|
||||
},
|
||||
registeredThrough: 'habitica-web',
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackRegistrationEvent', () => {
|
||||
afterEach(async () => {
|
||||
await RegistrationEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a registration event when a user registers', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000001';
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(1);
|
||||
expect(registrationEvents[0]).to.have.property('userId', user._id);
|
||||
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
|
||||
});
|
||||
|
||||
it('saves the correct data to the database', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000002';
|
||||
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSubscriptionEvent', () => {
|
||||
afterEach(async () => {
|
||||
await SubscriptionEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a subscription event when a user subscribes', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000003';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'subscribed',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_123',
|
||||
planId: 'plan_123',
|
||||
});
|
||||
|
||||
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
|
||||
expect(subscriptionEvents).to.have.lengthOf(1);
|
||||
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
|
||||
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
|
||||
});
|
||||
|
||||
it('creates a subscription event with cancellation reason when a user cancels', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000004';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'cancelled',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_456',
|
||||
planId: 'plan_456',
|
||||
cancellationReason: 'No longer needed',
|
||||
});
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
|
||||
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -66,13 +66,15 @@ describe('Amazon Payments - Cancel Subscription', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
@@ -30,12 +30,14 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
@@ -246,11 +248,6 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Add existing users
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Set expected amount
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
|
||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let iapIsCanceledStub;
|
||||
let iapIsExpiredStub;
|
||||
let paymentBuySkuStub;
|
||||
let iapGetPurchaseDataStub;
|
||||
let validateGiftMessageStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if no active subscription is found', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||
.returns(true);
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: 'subscription1month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let paymentBuySkuStub;
|
||||
let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.isExpired.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
const laterDate = moment.utc().add(7, 'days');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
.returns([{
|
||||
startTimeMillis: expirationDate.valueOf(),
|
||||
expirationDate,
|
||||
autoRenewing: false,
|
||||
}, {
|
||||
startTimeMillis: laterDate.valueOf(),
|
||||
expirationDate: laterDate,
|
||||
autoRenewing: false,
|
||||
},
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ autoRenewing: true }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ expirationDate, autoRenewing: false }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -128,11 +128,12 @@ describe('Purchasing a group plan for group', () => {
|
||||
expect(publicGroup.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = publicGroup._id;
|
||||
|
||||
// Public Guilds are no longer even findable
|
||||
await expect(api.createSubscription(data))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyPrivateGuildsCanUpgrade'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
|
||||
const updatedGroup = await Group.findById(publicGroup._id).exec();
|
||||
|
||||
@@ -3,7 +3,6 @@ import moment from 'moment';
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import common from '../../../../../website/common';
|
||||
import api from '../../../../../website/server/libs/payments/payments';
|
||||
import * as analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import * as notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -36,8 +36,6 @@ describe('payments/index', () => {
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
@@ -97,6 +95,16 @@ describe('payments/index', () => {
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvent).to.have.property('userId', recipient._id);
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('adds extra months to an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
@@ -298,28 +306,6 @@ describe('payments/index', () => {
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
@@ -455,6 +441,16 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('ipAddress');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
@@ -543,29 +539,24 @@ describe('payments/index', () => {
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
|
||||
});
|
||||
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom = { key: 'basic_earned' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -608,6 +599,23 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -1136,6 +1144,15 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
|
||||
@@ -30,13 +30,15 @@ describe('paypal - subscribeCancel', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ describe('Stripe - Checkout', () => {
|
||||
const group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
const groupId = group._id;
|
||||
@@ -376,11 +376,13 @@ describe('Stripe - Checkout', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
});
|
||||
|
||||
it('throws if user is not allowed to change group plan', async () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
@@ -315,12 +315,14 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analytics middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches analytics object to res', () => {
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics).to.exist;
|
||||
});
|
||||
|
||||
it('attaches stubbed methods for non-prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
|
||||
});
|
||||
|
||||
it('attaches real methods for prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
@@ -117,15 +117,6 @@ describe('Blocker middleware', () => {
|
||||
|
||||
checkIPBlockedErrorThrown(next);
|
||||
});
|
||||
|
||||
it('throws when the ip is blocked', () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkIPBlockedErrorThrown(next);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Blocking clients', () => {
|
||||
|
||||
@@ -50,5 +50,59 @@ describe('UserNotification Model', () => {
|
||||
expect(safeNotifications[0].type).to.equal('NEW_CHAT_MESSAGE');
|
||||
expect(safeNotifications[0].id).to.equal('123');
|
||||
});
|
||||
|
||||
it('removes duplicate STREAK_ACHIEVEMENT notifications', () => {
|
||||
// Fixes issue #13325 - Users receiving duplicate streak achievement notifications
|
||||
const notifications = [
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 123,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 456,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'CRON',
|
||||
id: 789,
|
||||
data: {},
|
||||
}), // different type, should be kept
|
||||
];
|
||||
|
||||
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
|
||||
expect(safeNotifications.length).to.equal(2);
|
||||
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
|
||||
expect(safeNotifications[0].id).to.equal('123');
|
||||
expect(safeNotifications[1].type).to.equal('CRON');
|
||||
expect(safeNotifications[1].id).to.equal('789');
|
||||
});
|
||||
|
||||
it('handles multiple STREAK_ACHIEVEMENT duplicates correctly', () => {
|
||||
// Test case: 3 duplicate STREAK_ACHIEVEMENT notifications
|
||||
const notifications = [
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 111,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 222,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 333,
|
||||
data: {},
|
||||
}),
|
||||
];
|
||||
|
||||
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
|
||||
expect(safeNotifications.length).to.equal(1);
|
||||
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
|
||||
expect(safeNotifications[0].id).to.equal('111'); // Keep first one
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /analytics/track/:eventName', () => {
|
||||
it('calls res.analytics', async () => {
|
||||
const user = await generateUser();
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
|
||||
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('POST /challenges/:challengeId/join', () => {
|
||||
it('returns error when challengeId is not a valid UUID', async () => {
|
||||
@@ -27,6 +29,37 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('public Guild', () => {
|
||||
let group;
|
||||
let groupLeader;
|
||||
let members;
|
||||
let challenge;
|
||||
before(async () => {
|
||||
({ group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
}));
|
||||
challenge = await generateChallenge(groupLeader, group);
|
||||
// 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 when challengeId is in an old public Guild', async () => {
|
||||
const authorizedUser = members[0]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
await expect(authorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a valid challenge', () => {
|
||||
let groupLeader;
|
||||
let group;
|
||||
@@ -66,6 +99,15 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
expect(res.name).to.equal(challenge.name);
|
||||
});
|
||||
|
||||
it('succeeds when it\'s a Tavern challenge, even if the user isn\'t a "member" of Tavern', async () => {
|
||||
const tavern = await groupLeader.get(`/groups/${TAVERN_ID}`);
|
||||
const tavernChallenge = await generateChallenge(groupLeader, tavern, { prize: 1 });
|
||||
const generalUser = await generateUser();
|
||||
|
||||
const res = await generalUser.post(`/challenges/${tavernChallenge._id}/join`);
|
||||
expect(res.name).to.equal(tavernChallenge.name);
|
||||
});
|
||||
|
||||
it('returns challenge data', async () => {
|
||||
const res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('GET /groups/:groupId/chat', () => {
|
||||
|
||||
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'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,9 +121,9 @@ describe('POST /chat/:chatId/like', () => {
|
||||
|
||||
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'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
|
||||
import { MAX_MESSAGE_LENGTH, CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
@@ -80,17 +80,20 @@ describe('POST /chat', () => {
|
||||
member.updateOne({ 'flags.chatRevoked': false });
|
||||
});
|
||||
|
||||
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
|
||||
it('errors when chat privileges are revoked when sending a message to a private guild', async () => {
|
||||
await member.updateOne({
|
||||
'flags.chatRevoked': true,
|
||||
});
|
||||
|
||||
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
await expect(member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not error when chat privileges are revoked when sending a message to a party', async () => {
|
||||
it('errors when chat privileges are revoked when sending a message to a party', async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
@@ -106,9 +109,12 @@ describe('POST /chat', () => {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
await expect(privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,7 +129,7 @@ describe('POST /chat', () => {
|
||||
member.updateOne({ 'flags.chatShadowMuted': false });
|
||||
});
|
||||
|
||||
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
|
||||
it('creates a chat with flagCount set when sending a message to a private guild', async () => {
|
||||
await member.updateOne({
|
||||
'flags.chatShadowMuted': true,
|
||||
});
|
||||
@@ -131,10 +137,10 @@ describe('POST /chat', () => {
|
||||
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.flagCount).to.eql(0);
|
||||
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
|
||||
});
|
||||
|
||||
it('creates a chat with zero flagCount when sending a message to a party', async () => {
|
||||
it('creates a chat with flagCount set when sending a message to a party', async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
@@ -153,7 +159,7 @@ describe('POST /chat', () => {
|
||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.flagCount).to.eql(0);
|
||||
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,6 +244,18 @@ describe('POST /chat', () => {
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with case-insensitive mentions', async () => {
|
||||
const originalUsername = member.auth.local.username;
|
||||
const uppercaseUsername = originalUsername.toUpperCase();
|
||||
const messageWithMentions = `hi @${uppercaseUsername}`;
|
||||
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
|
||||
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||
|
||||
expect(newMessage.message.id).to.exist;
|
||||
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with a max length of 3000 chars', async () => {
|
||||
const veryLongMessage = `
|
||||
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
requester,
|
||||
translate as t,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
|
||||
@@ -56,4 +57,28 @@ describe('GET /content', () => {
|
||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
});
|
||||
|
||||
describe('authenticated user', () => {
|
||||
let user;
|
||||
it('returns content in user\'s preferred language when no language parameter is provided', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'de' });
|
||||
const res = await user.get('/content');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
|
||||
});
|
||||
|
||||
it('respects language parameter over user\'s preferred language', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'de' });
|
||||
const res = await user.get('/content?language=fr');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr'));
|
||||
});
|
||||
|
||||
it('falls back to English if user\'s preferred language is invalid', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'invalid_lang' });
|
||||
const res = await user.get('/content');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
xdescribe('GET /export/avatar-:memberId.html', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
const dummyId = generateUUID();
|
||||
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', { userId: dummyId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an html page', async () => {
|
||||
const res = await user.get(`/export/avatar-${user._id}.html`);
|
||||
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// TODO how to test this route since it points to a file on AWS s3?
|
||||
|
||||
describe('GET /export/avatar-:memberId.png', () => {});
|
||||
@@ -38,7 +38,7 @@ describe('GET /export/inbox.html', () => {
|
||||
it('renders the markdown messages as html', async () => {
|
||||
const res = await user.get('/export/inbox.html');
|
||||
|
||||
expect(res).to.include('img class="habitica-emoji"');
|
||||
expect(res).to.include('😄');
|
||||
expect(res).to.include('<h1>Hello!</h1>');
|
||||
expect(res).to.include('<li>list 1</li>');
|
||||
});
|
||||
@@ -46,7 +46,7 @@ describe('GET /export/inbox.html', () => {
|
||||
it('sorts messages from newest to oldest', async () => {
|
||||
const res = await user.get('/export/inbox.html');
|
||||
|
||||
const emojiPosition = res.indexOf('img class="habitica-emoji"');
|
||||
const emojiPosition = res.indexOf('😄');
|
||||
const headingPosition = res.indexOf('<h1>Hello!</h1>');
|
||||
const listPosition = res.indexOf('<li>list 1</li>');
|
||||
|
||||
|
||||
@@ -61,6 +61,24 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
usernames: [userToInvite.auth.local.lowerCaseUsername],
|
||||
});
|
||||
expect(response).to.be.an('Array');
|
||||
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
|
||||
expect(response[0]._id).to.be.a('String');
|
||||
expect(response[0].id).to.eql(group._id);
|
||||
expect(response[0].name).to.eql(groupName);
|
||||
expect(response[0].inviter).to.eql(inviter._id);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
|
||||
});
|
||||
|
||||
it('invites a user to a group by username', async () => {
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
@@ -209,6 +227,24 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [userToInvite._id],
|
||||
});
|
||||
expect(response).to.be.an('Array');
|
||||
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
|
||||
expect(response[0]._id).to.be.a('String');
|
||||
expect(response[0].id).to.eql(group._id);
|
||||
expect(response[0].name).to.eql(groupName);
|
||||
expect(response[0].inviter).to.eql(inviter._id);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
|
||||
});
|
||||
|
||||
it('invites a user to a group by uuid', async () => {
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
@@ -281,6 +317,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending invite when inviter is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const res = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
emails: [testInvite],
|
||||
inviter: 'inviter name',
|
||||
});
|
||||
|
||||
const updatedUser = await inviterMuted.sync();
|
||||
|
||||
expect(res).to.exist;
|
||||
expect(updatedUser.invitesSent).to.eql(1);
|
||||
});
|
||||
|
||||
it('returns an error when invite is missing an email', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [{ name: 'test' }],
|
||||
@@ -405,6 +454,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const newUser = await generateUser();
|
||||
const invite = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [newUser._id],
|
||||
emails: [{ name: 'test', email: 'test@habitica.com' }],
|
||||
});
|
||||
const invitedUser = await newUser.get('/user');
|
||||
|
||||
expect(invitedUser.invitations.parties[0]).to.not.exist;
|
||||
expect(invite).to.exist;
|
||||
});
|
||||
|
||||
it('invites users to a group by uuid and email', async () => {
|
||||
const newUser = await generateUser();
|
||||
const invite = await inviter.post(`/groups/${group._id}/invite`, {
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
|
||||
it('returns four messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
promises.push(user.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: 'fourth',
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user has blocked the sender', async () => {
|
||||
it('returns error when recipient has blocked the sender', async () => {
|
||||
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
|
||||
|
||||
await expect(userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -56,7 +56,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when sender has blocked to user', async () => {
|
||||
it('returns error when sender has blocked recipient', async () => {
|
||||
const receiver = await generateUser();
|
||||
const sender = await generateUser({ 'inbox.blocks': [receiver._id] });
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user has opted out of messaging', async () => {
|
||||
it('returns error when recipient has opted out of messaging', async () => {
|
||||
const receiver = await generateUser({ 'inbox.optOut': true });
|
||||
|
||||
await expect(userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -174,7 +174,7 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(notification.data.excerpt).to.equal(messageExcerpt);
|
||||
});
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
it('allows admin to send when recipient has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
@@ -202,7 +202,7 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('allows admin to send when to user has opted out of messaging', async () => {
|
||||
it('allows admin to send when recipient has opted out of messaging', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
@@ -229,4 +229,58 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
describe('sender is shadow muted', () => {
|
||||
beforeEach(async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'flags.chatShadowMuted': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not save the message in the receiver inbox', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const response = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
expect(response.message.uuid).to.equal(receiver._id);
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(
|
||||
updatedReceiver.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
updatedSender.inbox.messages,
|
||||
message => message.uuid === receiver._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.not.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('does not save the message message twice if recipient is sender', async () => {
|
||||
const response = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: userToSendMessage._id,
|
||||
});
|
||||
|
||||
expect(response.message.uuid).to.equal(userToSendMessage._id);
|
||||
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
updatedSender.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
expect(Object.keys(updatedSender.inbox.messages).length).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -23,15 +22,4 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.get(appleEndpoint);
|
||||
const response = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
@@ -35,7 +35,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.get(appleEndpoint);
|
||||
const registerResponse = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
|
||||
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id, username: user.auth.local.username }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,6 +110,18 @@ describe('POST /user/auth/local/login', () => {
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
it('sets auth.timestamps.updated', async () => {
|
||||
const oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
// login
|
||||
await api.post(endpoint, {
|
||||
username: user.auth.local.email,
|
||||
password,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
});
|
||||
|
||||
it('user uses social authentication and has no password', async () => {
|
||||
await user.unset({
|
||||
'auth.local.hashed_password': 1,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
|
||||
import { encrypt } from '../../../../../../website/server/libs/encryption';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
function generateRandomUserName () {
|
||||
return (Date.now() + uuid()).substring(0, 20);
|
||||
@@ -41,6 +42,25 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
const user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
|
||||
});
|
||||
|
||||
it('registers a new user and sets verifiedUsername to true', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
translate as t,
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
@@ -64,6 +66,77 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const socialUser = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', socialUser.id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
|
||||
it('includes sanitized version of provided username', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Google User Name',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername');
|
||||
});
|
||||
|
||||
it('generates a random username if provided username contains only disallowed characters', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Áîüè',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('generates a random username if provided username contains a disallowed word', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'i am a TESTPLACEHOLDERSLURWORDHERE',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('generates a random username if sanitized username conflicts with an extant user', async () => {
|
||||
user = await generateUser({ 'auth.local.username': 'GoogleUserName' });
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Google User Name',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('fails if allowRegister is false and user does not exist', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: `${apiErrorMessages.socialFlowUserNotFound} ${user.auth.local.username}+google@example.com`,
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -131,6 +204,36 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if allowRegister is false', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.google.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -142,6 +245,17 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not track a registration event for existing users', async () => {
|
||||
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -167,5 +281,24 @@ describe('POST /user/auth/social', () => {
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
it('sets auth.timestamps.updated', async () => {
|
||||
let oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
|
||||
// Do it again to ensure it updates even when nothing else changes
|
||||
await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
|
||||
it('returns five messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
promises.push(user.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: 'fourth',
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
translate as t,
|
||||
requester,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v4';
|
||||
|
||||
const ENDPOINT = '/user/auth/check-email';
|
||||
|
||||
describe('POST /user/auth/check-email', () => {
|
||||
const email = 'SOmE-nEw-emAIl_2@example.net';
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
it('returns email if it is not used yet', async () => {
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email,
|
||||
});
|
||||
expect(response.email).to.eql(email);
|
||||
expect(response.valid).to.be.true;
|
||||
});
|
||||
|
||||
it('rejects if email is not provided', async () => {
|
||||
await expect(api.post(ENDPOINT, {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid request parameters.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if email is already taken', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: user.auth.local.email,
|
||||
});
|
||||
expect(response).to.eql({
|
||||
valid: false,
|
||||
email: user.auth.local.email,
|
||||
error: t('cannotFulfillReq'),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if casing is different', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: user.auth.local.email.toUpperCase(),
|
||||
});
|
||||
expect(response).to.eql({
|
||||
valid: false,
|
||||
email: user.auth.local.email.toUpperCase(),
|
||||
error: t('cannotFulfillReq'),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if email uses restricted domain', async () => {
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: 'fake@habitica.com',
|
||||
});
|
||||
expect(response.valid).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import md from 'habitica-markdown';
|
||||
|
||||
describe('habiticaMarkdown emoji plugin', () => {
|
||||
it('renders standard emoji as Unicode', () => {
|
||||
const result = md.render(':smile:');
|
||||
expect(result).to.include('😄');
|
||||
expect(result).not.to.include('img');
|
||||
});
|
||||
|
||||
it('renders thumbsup emoji as Unicode', () => {
|
||||
const result = md.render(':thumbsup:');
|
||||
expect(result).to.include('👍');
|
||||
});
|
||||
|
||||
it('renders +1 emoji as Unicode', () => {
|
||||
const result = md.render(':+1:');
|
||||
expect(result).to.include('👍');
|
||||
});
|
||||
|
||||
it('renders melior as an img tag', () => {
|
||||
const result = md.render(':melior:');
|
||||
expect(result).to.include('<img class="habitica-emoji"');
|
||||
expect(result).to.include('src="https://s3.amazonaws.com/habitica-assets/cdn/emoji/melior.png"');
|
||||
expect(result).to.include('alt="melior"');
|
||||
});
|
||||
|
||||
it('does NOT convert emoji inside markdown links', () => {
|
||||
const result = md.render('[:smile: link](http://example.com)');
|
||||
expect(result).to.include(':smile: link');
|
||||
expect(result).not.to.include('😄');
|
||||
});
|
||||
|
||||
it('converts emoji outside of links normally', () => {
|
||||
const result = md.render(':smile: [link](http://example.com)');
|
||||
expect(result).to.include('😄');
|
||||
expect(result).to.include('link');
|
||||
});
|
||||
|
||||
it('leaves removed custom emoji (bowtie) as literal text', () => {
|
||||
const result = md.render(':bowtie:');
|
||||
expect(result).to.include(':bowtie:');
|
||||
expect(result).not.to.include('img');
|
||||
});
|
||||
|
||||
it('leaves unknown shortcodes as literal text', () => {
|
||||
const result = md.render(':nonexistent_emoji_xyz:');
|
||||
expect(result).to.include(':nonexistent_emoji_xyz:');
|
||||
});
|
||||
|
||||
it('renders new emoji not in the old dataset', () => {
|
||||
const result = md.render(':yawning_face:');
|
||||
expect(result).to.include('🥱');
|
||||
});
|
||||
|
||||
it('supports unsafeHTMLRender', () => {
|
||||
const result = md.unsafeHTMLRender('<b>bold</b> :smile:');
|
||||
expect(result).to.include('<b>bold</b>');
|
||||
expect(result).to.include('😄');
|
||||
});
|
||||
|
||||
it('supports renderWithMentions', () => {
|
||||
const result = md.renderWithMentions(':smile: @testuser', { userName: 'testuser' });
|
||||
expect(result).to.include('😄');
|
||||
expect(result).to.include('at-text');
|
||||
expect(result).to.include('at-highlight');
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -32,12 +31,6 @@ describe('shared.ops.buy', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', async () => {
|
||||
@@ -51,10 +44,8 @@ describe('shared.ops.buy', () => {
|
||||
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
await buy(user, { params: { key: 'potion' } });
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', async () => {
|
||||
|
||||
@@ -29,10 +29,9 @@ describe('shared.ops.buyArmoire', () => {
|
||||
const YIELD_EQUIPMENT = 0.5;
|
||||
const YIELD_FOOD = 0.7;
|
||||
const YIELD_EXP = 0.9;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyArmoire (_user, _req, _analytics) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
|
||||
async function buyArmoire (_user, _req) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -50,12 +49,10 @@ describe('shared.ops.buyArmoire', () => {
|
||||
user.items.food = {};
|
||||
|
||||
sandbox.stub(randomValFns, 'trueRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
randomValFns.trueRandom.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -147,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
await buyArmoire(user, {}, analytics);
|
||||
await buyArmoire(user, {});
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(3);
|
||||
|
||||
@@ -155,7 +152,6 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(analytics.track).to.be.calledTwice;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
@@ -11,15 +10,14 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
|
||||
async function buyGem (user, req, analytics) {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
async function buyGem (user, req) {
|
||||
const buyOp = new BuyGemOperation(user, req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyGem', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
const goldPoints = 40;
|
||||
const gemsBought = 40;
|
||||
const userGemAmount = 10;
|
||||
@@ -35,23 +33,16 @@ describe('shared.ops.buyGem', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Gems', () => {
|
||||
it('purchases gems', async () => {
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', async () => {
|
||||
|
||||
@@ -10,10 +10,9 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyHealthPotion', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyHealthPotion (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
|
||||
async function buyHealthPotion (_user, _req) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -32,19 +31,13 @@ describe('shared.ops.buyHealthPotion', () => {
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Potion', () => {
|
||||
it('recovers 15 hp', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buyHealthPotion(user, {}, analytics);
|
||||
await buyHealthPotion(user, {});
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('does not increase hp above 50', async () => {
|
||||
|
||||
@@ -13,15 +13,14 @@ import {
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
async function buyGear (user, req, analytics) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
async function buyGear (user, req) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -47,14 +46,12 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
@@ -65,7 +62,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
it('adds equipment to inventory', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
@@ -92,13 +89,12 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
@@ -111,7 +107,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -27,11 +26,9 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -93,7 +90,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
context('successful purchases', () => {
|
||||
it('buys Steampunk Accessories Set', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
|
||||
await buyMysterySet(user, { params: { key: '301404' } });
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
@@ -106,7 +103,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
it('buys mystery set if it is available', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||
await buyMysterySet(user, { params: { key: '201601' } });
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||
|
||||
@@ -12,10 +12,9 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -25,13 +24,11 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
@@ -12,21 +12,15 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyQuest', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('buys a Quest scroll', async () => {
|
||||
@@ -35,12 +29,11 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||
@@ -49,10 +42,9 @@ describe('shared.ops.buyQuest', () => {
|
||||
user.items.quests[key] = -1;
|
||||
await buyQuest(user, {
|
||||
params: { key },
|
||||
}, analytics);
|
||||
});
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
|
||||
@@ -61,13 +53,13 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: '3',
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 4,
|
||||
@@ -82,7 +74,7 @@ describe('shared.ops.buyQuest', () => {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: 'a',
|
||||
}, analytics);
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -187,12 +179,11 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress3: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,20 +14,17 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
async function buySpecialSpell (_user, _req) {
|
||||
const buyOp = new BuySpellOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -78,7 +75,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
@@ -89,7 +86,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a limited card when it is available', async () => {
|
||||
@@ -101,7 +97,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.nye).to.equal(1);
|
||||
@@ -112,7 +108,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the card is not currently available', async () => {
|
||||
@@ -140,7 +135,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.seafoam).to.equal(1);
|
||||
@@ -151,7 +146,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the spell is not currently available', async () => {
|
||||
|
||||
@@ -13,21 +13,15 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
|
||||
|
||||
describe('common.ops.hourglassPurchase', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyMount (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
|
||||
async function buyMount (_user, _req) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -131,12 +125,11 @@ describe('common.ops.hourglassPurchase', () => {
|
||||
it('buys a pet', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 2;
|
||||
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
|
||||
|
||||
expect(message).to.eql(i18n.t('hourglassPurchase'));
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a mount', async () => {
|
||||
|
||||
@@ -17,20 +17,17 @@ describe('shared.ops.purchase', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
before(() => {
|
||||
user = generateUser({ 'stats.class': 'rogue' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
@@ -187,11 +184,10 @@ describe('shared.ops.purchase', () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
await purchase(user, { params: { type, key } }, analytics);
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases hatchingPotions', async () => {
|
||||
@@ -332,7 +328,7 @@ describe('shared.ops.purchase', () => {
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -345,7 +341,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: -2 });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -358,7 +354,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
|
||||
@@ -211,22 +211,32 @@ describe('shared.ops.rebirth', () => {
|
||||
expect(user.achievements.rebirthLevel).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not increment rebirth achievements when level is lower than previous', async () => {
|
||||
it('increments rebirth achievements even when level is lower than previous', async () => {
|
||||
user.stats.lvl = 2;
|
||||
user.achievements.rebirths = 1;
|
||||
user.achievements.rebirthLevel = 3;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
expect(user.achievements.rebirths).to.equal(1);
|
||||
expect(user.achievements.rebirths).to.equal(2);
|
||||
expect(user.achievements.rebirthLevel).to.equal(3);
|
||||
});
|
||||
|
||||
it('always increments rebirth achievements when level is MAX_LEVEL', async () => {
|
||||
it('updates rebirthLevel when current level is higher than previous', async () => {
|
||||
user.stats.lvl = 5;
|
||||
user.achievements.rebirths = 1;
|
||||
user.achievements.rebirthLevel = 3;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
expect(user.achievements.rebirths).to.equal(2);
|
||||
expect(user.achievements.rebirthLevel).to.equal(5);
|
||||
});
|
||||
|
||||
it('increments rebirth achievements when level is MAX_LEVEL', async () => {
|
||||
user.stats.lvl = MAX_LEVEL;
|
||||
user.achievements.rebirths = 1;
|
||||
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
|
||||
user.achievements.rebirthLevel = MAX_LEVEL + 1;
|
||||
user.achievements.rebirthLevel = MAX_LEVEL;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
@@ -234,11 +244,10 @@ describe('shared.ops.rebirth', () => {
|
||||
expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL);
|
||||
});
|
||||
|
||||
it('always increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
|
||||
it('increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
|
||||
user.stats.lvl = MAX_LEVEL + 1;
|
||||
user.achievements.rebirths = 1;
|
||||
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
|
||||
user.achievements.rebirthLevel = MAX_LEVEL + 2;
|
||||
user.achievements.rebirthLevel = MAX_LEVEL;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ describe('shared.ops.unlock', () => {
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.balance = usersStartingGems;
|
||||
user.pinnedItems.push({ type: 'background', path: 'backgrounds.backgrounds042016.giant_florals' });
|
||||
user.pinnedItems.push({ type: 'haircolor', path: 'hair.color.rainbow' });
|
||||
user.pinnedItems.push({ type: 'shirt', path: 'shirt.convict' });
|
||||
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
|
||||
});
|
||||
|
||||
@@ -272,6 +275,7 @@ describe('shared.ops.unlock', () => {
|
||||
});
|
||||
|
||||
it('unlocks an item (appearance)', async () => {
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.not.equal(-1);
|
||||
const path = unlockPath.split(',')[0];
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
@@ -282,11 +286,12 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('unlocks an item (hair color)', async () => {
|
||||
user.purchased.hair.color = {};
|
||||
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.not.equal(-1);
|
||||
const path = hairUnlockPath.split(',')[0];
|
||||
const initialColorHair = Object.keys(user.purchased.hair.color).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
@@ -297,6 +302,7 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('unlocks an item (facial hair)', async () => {
|
||||
@@ -334,6 +340,7 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
it('unlocks an item (background)', async () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.not.equal(-1);
|
||||
const [, message] = await unlock(user, {
|
||||
query: { path: backgroundUnlockPath },
|
||||
});
|
||||
@@ -344,6 +351,7 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('handles an invalid hair path gracefully', async () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getMatchingSwap, makeSubstitutionMap } from '../../website/common/script/content/constants/aprilFools';
|
||||
|
||||
describe('April Fools', () => {
|
||||
describe('getMatchingSwap', () => {
|
||||
it('returns Veggie for 2020', () => {
|
||||
const swap = getMatchingSwap(new Date('2020-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
it('returns Alien for 2026', () => {
|
||||
const swap = getMatchingSwap(new Date('2026-04-01'));
|
||||
expect(swap).to.equal('Alien');
|
||||
});
|
||||
it('Cycles through swaps correctly', () => {
|
||||
const swap = getMatchingSwap(new Date('2027-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeSubstitutionMap', () => {
|
||||
it('returns correct substitution for Veggie', () => {
|
||||
const substitutions = makeSubstitutionMap('Veggie');
|
||||
expect(substitutions.pets['Pet-Wolf-']).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets['Pet-TigerCub-']).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Veggie');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Veggie');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets.noPetIOS).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Veggie');
|
||||
});
|
||||
|
||||
it('returns correct substitution for Cryptid', () => {
|
||||
const substitutions = makeSubstitutionMap('Cryptid');
|
||||
expect(substitutions.pets['Pet-Fox-']).to.equal('Pet-Fox-Cryptid');
|
||||
expect(substitutions.pets['Pet-FlyingPig-']).to.equal('Pet-FlyingPig-Cryptid');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Cryptid');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Cryptid');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Cryptid');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Cryptid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -54,19 +54,4 @@ describe('armoire', () => {
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
if (setMap[item.set] === undefined) {
|
||||
setMap[item.set] = 0;
|
||||
}
|
||||
setMap[item.set] += 1;
|
||||
});
|
||||
Object.keys(setMap).forEach(set => {
|
||||
expect(setMap[set], set).to.be.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { STRING_ERROR_MSG, STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
|
||||
import { STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
|
||||
import translator from '../../website/common/script/content/translation';
|
||||
|
||||
describe('Translator', () => {
|
||||
it('returns error message if string is not properly formatted', () => {
|
||||
const improperlyFormattedString = translator('petName', { attr: 0 })();
|
||||
expect(improperlyFormattedString).to.match(STRING_ERROR_MSG);
|
||||
});
|
||||
|
||||
it('returns an error message if string does not exist', () => {
|
||||
const stringDoesNotExist = translator('stringDoesNotExist')();
|
||||
expect(stringDoesNotExist).to.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
|
||||
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/paypal') === 0
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
import './globals.helper';
|
||||
import { translations } from '../../website/server/libs/i18n';
|
||||
import { contentTranslations } from '../../website/server/libs/i18n';
|
||||
|
||||
i18n.translations = translations;
|
||||
i18n.translations = contentTranslations;
|
||||
|
||||
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
|
||||
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function getProperty (collectionName, id, path) {
|
||||
// Specifically helpful for the GET /groups tests,
|
||||
// resets the db to an empty state and creates a tavern document
|
||||
export async function resetHabiticaDB () {
|
||||
console.info('Resetting Habitica DB');
|
||||
const groups = mongoose.connection.db.collection('groups');
|
||||
const users = mongoose.connection.db.collection('users');
|
||||
return mongoose.connection.dropDatabase()
|
||||
|
||||
@@ -12,20 +12,12 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
// TODO find a way to let eslint understand webpack aliases
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': 'off',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
// this creates issues with the current way we have to push the process.env vars to webpack
|
||||
// https://github.com/eslint/eslint/issues/14918
|
||||
// https://github.com/webpack/webpack/issues/5392
|
||||
// off for now, because any eslint --fix will then still do it anyway
|
||||
// maybe this can be turned on again once we switch to newer vue/vite
|
||||
// Important! process.env.XYZ should not be destructured
|
||||
'prefer-destructuring': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
@@ -32,6 +32,6 @@
|
||||
|
||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||
<!-- Translations -->
|
||||
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
|
||||
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"serve:docker": "npx vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
@@ -27,12 +28,13 @@
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"habitica-markdown": "^4.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.0.0",
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
@@ -41,10 +43,9 @@
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -58,8 +59,6 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import snackbars from '@/components/snackbars/notifications';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
@@ -150,10 +149,6 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
Analytics.load();
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
@@ -208,17 +203,37 @@ export default {
|
||||
|
||||
return response;
|
||||
}, error => { // Set up Error interceptors
|
||||
if (!error.response) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (error.response.status >= 400) {
|
||||
const isBanned = this.checkForBannedUser(error);
|
||||
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
||||
|
||||
// Don't show errors from getting user details. These users have delete their account,
|
||||
// Don't show errors from getting user details. These users have deleted their account,
|
||||
// but their chat message still exists.
|
||||
const configExists = Boolean(error.response) && Boolean(error.response.config);
|
||||
if (configExists && error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// Chat paging should help this, but maybe we can also find another solution..
|
||||
return Promise.resolve(error);
|
||||
if (configExists) {
|
||||
if (error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// Chat paging should help this, but maybe we can also find another solution..
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
// Also, a 404 occurs during routine attempt to log in with social,
|
||||
// when we check for account already existing.
|
||||
if (error.response.config.method === 'post' && (error.response.config.url.indexOf('/api/v4/user/auth/social') !== -1
|
||||
|| error.response.config.url.indexOf('/api/v4/user/auth/apple') !== -1)) {
|
||||
const socialEmail = error.response.data.message.split(': ')[1];
|
||||
if (socialEmail) {
|
||||
window.sessionStorage.setItem('social-email', socialEmail);
|
||||
}
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
if (error.response.status === 404
|
||||
&& error.response.config.method === 'get'
|
||||
&& error.response.config.url.indexOf('/api/v4/groups/party') !== -1) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const errorData = error.response.data;
|
||||
|
||||
@@ -22,8 +22,15 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
|
||||
.Pet_HatchingPotion_Alien {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -52,6 +59,10 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
|
||||
@@ -635,6 +635,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_swamp {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_swamp.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_tree_tunnel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_tree_tunnel.png');
|
||||
width: 141px;
|
||||
@@ -810,6 +815,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_castle_keep_with_banners {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_castle_keep_with_banners.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_cemetery_gate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_cemetery_gate.png');
|
||||
width: 141px;
|
||||
@@ -1045,6 +1055,16 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_elegant_palace {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elegant_palace.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_elven_citadel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elven_citadel.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_enchanted_music_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
|
||||
width: 141px;
|
||||
@@ -1546,6 +1566,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_inside_forest_witchs_cottage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_inside_forest_witchs_cottage.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_iridescent_clouds {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_iridescent_clouds.png');
|
||||
width: 141px;
|
||||
@@ -1736,6 +1761,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_nighttime_street_with_shops {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_nighttime_street_with_shops.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_ocean_sunrise {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_ocean_sunrise.png');
|
||||
width: 141px;
|
||||
@@ -1771,6 +1801,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_a_strange_planet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_strange_planet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_tree_branch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
|
||||
width: 141px;
|
||||
@@ -1906,6 +1941,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_riding_a_comet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_riding_a_comet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_rime_ice {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rime_ice.png');
|
||||
width: 141px;
|
||||
@@ -2402,6 +2442,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_waterfall_with_rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_waterfall_with_rainbow.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_wedding_arch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_wedding_arch.png');
|
||||
width: 141px;
|
||||
@@ -2422,6 +2467,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_winter_desert_with_saguaros {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_desert_with_saguaros.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_winter_fireworks {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_fireworks.png');
|
||||
width: 141px;
|
||||
@@ -29440,6 +29490,11 @@
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.back_armoire_harpsichord {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_armoire_harpsichord.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.body_armoire_clownsBowtie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_armoire_clownsBowtie.png');
|
||||
width: 114px;
|
||||
@@ -29565,6 +29620,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blackPartyDress {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blackPartyDress.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blacksmithsApron {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blacksmithsApron.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -29755,6 +29820,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -29820,6 +29890,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_loneCowpokeOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_loneCowpokeOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_lunarArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lunarArmor.png');
|
||||
width: 90px;
|
||||
@@ -30000,6 +30075,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softOrangeSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softOrangeSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softPinkSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softPinkSuit.png');
|
||||
width: 114px;
|
||||
@@ -30020,6 +30100,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -30200,11 +30285,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blackHairbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackHairbow.png');
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blackSpookySorceryHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackSpookySorceryHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blacksmithsGoggles {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blacksmithsGoggles.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blueFloppyHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blueFloppyHat.png');
|
||||
width: 90px;
|
||||
@@ -30315,6 +30410,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_floppyOrangeHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyOrangeHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_floppyYellowHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyYellowHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_flutteryWig {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
|
||||
width: 114px;
|
||||
@@ -30435,6 +30540,11 @@
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.head_armoire_loneCowpokeHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_loneCowpokeHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_lunarCrown {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lunarCrown.png');
|
||||
width: 90px;
|
||||
@@ -30630,6 +30740,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_verdantArmingCap {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_verdantArmingCap.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_vermilionArcherHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_vermilionArcherHelm.png');
|
||||
width: 90px;
|
||||
@@ -30745,6 +30860,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_doubleBass {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_doubleBass.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_dragonTamerShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dragonTamerShield.png');
|
||||
width: 90px;
|
||||
@@ -30950,6 +31070,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_prettyPinkGiftBox {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_prettyPinkGiftBox.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_ramHornShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_ramHornShield.png');
|
||||
width: 90px;
|
||||
@@ -31010,6 +31135,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softOrangePillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softOrangePillow.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softPinkPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softPinkPillow.png');
|
||||
width: 114px;
|
||||
@@ -31030,6 +31160,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softYellowPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softYellowPillow.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_spanishGuitar {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
|
||||
width: 114px;
|
||||
@@ -31080,6 +31215,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_verdantBanner {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_verdantBanner.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_vikingShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_vikingShield.png');
|
||||
width: 90px;
|
||||
@@ -31150,6 +31290,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blackPartyDress {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blackPartyDress.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blacksmithsApron {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blacksmithsApron.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -31340,6 +31490,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -31405,6 +31560,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_loneCowpokeOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_loneCowpokeOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_lunarArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lunarArmor.png');
|
||||
width: 90px;
|
||||
@@ -31585,6 +31745,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softOrangeSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softOrangeSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softPinkSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softPinkSuit.png');
|
||||
width: 114px;
|
||||
@@ -31605,6 +31770,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -31695,6 +31865,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_bambooFlute {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_bambooFlute.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_barristerGavel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_barristerGavel.png');
|
||||
width: 90px;
|
||||
@@ -31735,6 +31910,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_blacksmithsHammer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blacksmithsHammer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_blueKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blueKite.png');
|
||||
width: 114px;
|
||||
@@ -32100,6 +32280,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_prettyPinkParasol {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_prettyPinkParasol.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pushBroom {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
|
||||
width: 114px;
|
||||
@@ -33025,6 +33210,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33255,6 +33460,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33425,6 +33650,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33640,6 +33880,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33860,6 +34120,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33895,6 +34175,81 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202601 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202601.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202602 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202602.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202602 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202602.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202601 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202601.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_201402 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
|
||||
width: 90px;
|
||||
@@ -35665,6 +36020,41 @@
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.back_mystery_202510 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202510.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.body_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.eyewear_mystery_202510 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202510.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202511 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202511.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_mystery_202511 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202511.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
@@ -35980,6 +36370,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36300,6 +36710,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36485,6 +36915,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36720,6 +37165,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36960,6 +37425,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -38440,6 +38925,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -38735,6 +39240,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -38915,6 +39440,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -39155,6 +39695,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.slim_armor_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -39395,6 +39955,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -52648,6 +53228,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -53138,6 +53723,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Amber.png');
|
||||
width: 81px;
|
||||
@@ -53928,6 +54518,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54423,6 +55018,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54758,6 +55358,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Amber.png');
|
||||
width: 81px;
|
||||
@@ -55538,6 +56143,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -56143,6 +56753,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -57538,6 +58153,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -58183,6 +58803,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Amber.png');
|
||||
width: 81px;
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="378" height="176" viewBox="0 0 378 176" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0H378V174C378 175.105 377.105 176 376 176H1.99999C0.895423 176 0 175.105 0 174V0Z" fill="url(#paint0_linear_2257_239)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2257_239" x1="378" y1="0" x2="0" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#925CF3"/>
|
||||
<stop offset="1" stop-color="#34B5C1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
@@ -0,0 +1,37 @@
|
||||
<svg width="48" height="96" viewBox="0 0 48 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.10104 12.0483C-2.82088 9.43721 -3.53422 6.57214 -5.6115 5.24584C-7.68877 3.91954 -9.89543 4.92709 -10.1422 6.808C-10.3891 8.68891 -9.06061 9.83066 -4.97737 13.9337C-3.81821 15.0985 -3.3812 14.6594 -3.10104 12.0483Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.34089 15.2054C4.45116 13.6561 7.27707 12.8443 9.45877 13.9889C11.6405 15.1334 11.8754 17.5575 10.3778 18.7127C8.88016 19.868 7.23193 19.2828 1.65411 17.781C0.0706697 17.3546 0.230624 16.7548 2.34089 15.2054Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.549002 12.0098C-3.61871 9.59194 -3.87667 15.8322 -2.20457 16.8023C-0.532473 17.7724 4.71671 14.4277 0.549002 12.0098Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L13.637 24.9825L9.18965 32.7229L-6.21656 23.785L-1.76917 16.0445Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.90457 13.0652L3.36623 19.0238L-1.08116 26.7643L-11.352 20.8057L-6.90457 13.0652Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L3.36623 19.0238L1.88377 21.604L-3.25163 18.6247L-1.76917 16.0445Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.21656 23.785L6.62195 31.2333L-3.75529 49.2944L-16.5938 41.8461L-6.21656 23.785Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.64886 25.2747L6.62195 31.2333L5.13948 33.8134L-5.13132 27.8548L-3.64886 25.2747Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.401307 24.1842L10.6721 30.1428L9.18965 32.7229L-1.08116 26.7643L0.401307 24.1842Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7924 38.4607L17.9387 42.0519L21.31 40.5834L24.8838 41.4413L23.4225 38.0537L24.2762 34.4625L20.9049 35.9309L17.3311 35.0731L18.7924 38.4607Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.93867 71.2331L-4.79238 74.8243L-1.42111 73.3559L2.15271 74.2137L0.691383 70.8261L1.54509 67.2349L-1.82618 68.7033L-5.4 67.8455L-3.93867 71.2331Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8949 25.3807L35.0583 29.8802L37.9424 26.2452L42.4202 25.0761L38.8028 22.178L37.6393 17.6786L34.7552 21.3135L30.2775 22.4826L33.8949 25.3807Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L40.579 68.1435L45.9507 88.2881L31.6312 92.1436L26.2596 71.999Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L26.2589 71.9966L31.6273 92.1421L17.3084 96L11.9401 75.8545Z" fill="#DDF3F3"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 23.3957 72.7701)" fill="#FFA624"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 26.2596 71.999)" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.9999 90.0369L30.8638 89.2658L31.6312 92.1436L28.7673 92.9147L27.9999 90.0369Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3957 72.7701L26.2596 71.999L27.0269 74.8768L24.163 75.6479L23.3957 72.7701Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L23.3951 72.7682L24.162 75.6461L12.707 78.7325L11.9401 75.8545Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5443 93.1213L27.9999 90.0369L28.7673 92.9147L17.3117 95.9991L16.5443 93.1213Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.1235 71.2279L40.579 68.1435L41.3464 71.0213L29.8908 74.1057L29.1235 71.2279Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.7277 88.4947L45.1833 85.4103L45.9507 88.2881L34.4951 91.3725L33.7277 88.4947Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.8638 89.2658L33.7277 88.4947L34.4951 91.3725L31.6312 92.1436L30.8638 89.2658Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L29.1235 71.2279L29.8908 74.1057L27.0269 74.8768L26.2596 71.999Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5224 56.3076C25.8087 53.7812 24.0792 51.3933 21.6588 50.9455C19.2383 50.4977 17.5679 52.2625 18.0403 54.0994C18.5126 55.9363 20.17 56.4948 25.4855 58.7621C26.9945 59.4057 27.236 58.834 26.5224 56.3076Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.745 57.1864C34.124 54.9555 36.4415 53.1391 38.8911 53.3791C41.3406 53.6191 42.4621 55.7782 41.5042 57.413C40.5463 59.0479 38.7999 59.1258 33.0684 59.8329C31.4413 60.0337 31.366 59.4173 32.745 57.1864Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8923 54.898C25.1267 54.225 27.2139 60.108 29.1258 60.378C31.0378 60.648 34.6579 55.571 29.8923 54.898Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L46.8635 61.9994L45.6255 70.8503L28.0091 68.3625L29.247 59.5115Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6306 57.0236L29.247 59.5114L28.0091 68.3624L10.3927 65.8745L11.6306 57.0236Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L35.1192 60.3408L33.8813 69.1917L22.137 67.5332L23.3749 58.6822Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.0091 68.3625L22.137 67.5332L23.3749 58.6822Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L35.1192 60.3408L34.7065 63.2911L28.8344 62.4618L29.247 59.5115Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.8344 62.4618L22.9622 61.6326L23.3749 58.6822Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8053 62.9241L22.5496 64.5827L22.137 67.533L10.3927 65.8745L10.8053 62.9241Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2939 66.2414L46.0382 67.9L45.6255 70.8503L33.8813 69.1917L34.2939 66.2414Z" fill="#DDF3F3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 375 B |
@@ -10,7 +10,7 @@
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
color: $white;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover:not(:disabled):not(.disabled), &:focus {
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
|
||||
&.btn-flat {
|
||||
@@ -28,14 +28,25 @@
|
||||
|
||||
&:disabled, &.disabled {
|
||||
cursor: default;
|
||||
color: $gray-50;
|
||||
opacity: 0.75;
|
||||
box-shadow: none;
|
||||
background-color: $gray-700;
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba($black, 0.12),
|
||||
0 1px 2px 0 rgba($black, 0.24);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 4px 12px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
.svg {
|
||||
color: $gray-300;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +175,6 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: $maroon-100;
|
||||
border: 2px solid transparent;
|
||||
@@ -242,29 +252,32 @@
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: $blue-50;
|
||||
background-color: $blue-100;
|
||||
color: $black;
|
||||
font-weight: 700;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
&:disabled {
|
||||
background: $blue-50;
|
||||
background-color: $white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
|
||||
&:focus {
|
||||
background: $blue-100;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
color: $black;
|
||||
}
|
||||
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: $blue-100;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
background-color: $blue-50;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(.disabled), &.active:not(:disabled):not(.disabled) {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Inputs and textareas
|
||||
|
||||
input, textarea, input.form-control, textarea.form-control {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
padding: 4px 12px;
|
||||
color: $gray-50;
|
||||
border: 1px solid $gray-400;
|
||||
|
||||
&:hover:not(:disabled):not(:read-only):not(:focus):not(:disabled):not(.input-valid):not(.input-invalid):not(.dark) {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only),
|
||||
&:active:not(:disabled):not(:read-only).dark, &:focus:not(:disabled):not(:read-only).dark {
|
||||
border: 1px solid $purple-400;
|
||||
outline: 1px solid $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 27px;
|
||||
background-image: url(@/assets/svg/for-css/check.svg);
|
||||
background-size: 1rem;
|
||||
border-color: $green-10;
|
||||
}
|
||||
|
||||
&.input-invalid, .input-invalid:hover {
|
||||
padding-right: 40px;
|
||||
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||
background-size: 16px 16px;
|
||||
border-color: $red-100;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.input-invalid.input-with-error {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
|
||||
&:not(.input-valid):not(.input-invalid) {
|
||||
border: 1px solid $purple-300;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover:not(:focus):not(:disabled):not(.input-valid):not(.input-invalid) {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $purple-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $maroon-500;
|
||||
}
|
||||
|
||||
// checkboxes
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
&:hover~.custom-control-label::before {
|
||||
border-color: $gray-100;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-300;
|
||||
border-color: $purple-300;
|
||||
}
|
||||
|
||||
&:hover:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:disabled)~.custom-control-label::before {
|
||||
background-color: $purple-400;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
}
|
||||
|
||||
&:checked:disabled~.custom-control-label::after {
|
||||
background-image: url(@/assets/svg/for-css/checkbox-gray.svg);
|
||||
}
|
||||
|
||||
&:active~.custom-control-label::before {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
~.custom-control-label::before {
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:hover~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-50;
|
||||
}
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-100;
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,11 @@ h3.markdown {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.emoji-native {
|
||||
font-size: 0.85em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 16px;
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.d-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.privacy-banner {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: $white;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
|
||||
width: calc(66vw + 96px);
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
margin: auto 12.5%;
|
||||
}
|
||||
@media only screen and (min-width: 992px) {
|
||||
margin: auto 14.5%;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
li, p {
|
||||
li, p:not(.purple-600) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,10 @@ h4 {
|
||||
background-color: $green-100 !important;
|
||||
}
|
||||
|
||||
.bg-purple-50 {
|
||||
background-color: $purple-50 !important;
|
||||
}
|
||||
|
||||
.bg-purple-100 {
|
||||
background-color: $purple-100 !important;
|
||||
}
|
||||
@@ -119,6 +123,10 @@ h4 {
|
||||
background-color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
background-color: $yellow-50 !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: $white !important;
|
||||
}
|
||||
@@ -131,6 +139,10 @@ h4 {
|
||||
color: $gray-50 !important;
|
||||
}
|
||||
|
||||
.gray-100 {
|
||||
color: $gray-100 !important;
|
||||
}
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="#FFFFFF" fill-rule="nonzero">
|
||||
<polygon points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 10">
|
||||
<path fill="#878190" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.83-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.1792 31.6843L46.8536 22.3769L23.918 28.6988L18.861 42.5218L44.341 58.5813L58.1792 31.6843Z" fill="#FF944C"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L46.1108 26.1328L36.2812 28.8422L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M30.2393 39.0304L26.4518 31.5515L36.2813 28.8422L30.2393 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L36.2813 28.8422L30.2393 39.0304L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.1108 26.1328L46.6218 34.5148L53.8301 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L26.4518 31.5516L30.2393 39.0304L23.0309 41.0173Z" fill="#FA8537"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.6218 34.5148L43.0424 53.79L53.8301 32.5279Z" fill="#FA8537"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L30.2393 39.0304L43.0425 53.79L23.0309 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L30.2393 39.0304L43.0425 53.79L46.6218 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.555 4.15937L47.026 0.420004L38.7773 1.59601L36.4144 6.17539L44.5675 12.8919L50.555 4.15937Z" fill="#FFBE5D"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L46.6034 1.6924L43.0682 2.1964L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M40.5221 5.46854L39.5331 2.7004L43.0682 2.1964L40.5221 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L43.0683 2.1964L40.5221 5.46855L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25894L46.6034 1.6924L46.414 4.62854L49.0064 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M37.9296 5.83815L39.5331 2.70041L40.5221 5.46855L37.9296 5.83815Z" fill="#FFA624"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25893L46.414 4.62853L44.3259 11.1688L49.0064 4.25893Z" fill="#FFA624"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M37.9297 5.83815L40.5221 5.46855L44.326 11.1688L37.9297 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L40.5221 5.46855L44.326 11.1688L46.414 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2986 16.7775L24.6513 8.36623L11.1016 3.94533L4.07056 9.19883L11.614 25.6769L27.2986 16.7775Z" fill="#FF6165"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L23.0573 10.0026L17.2502 8.10789L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M10.908 11.2141L11.4432 6.21322L17.2502 8.10789L10.908 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L17.2502 8.10789L10.9081 11.2141L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L23.0573 10.0026L20.5864 14.3719L24.8449 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M6.64955 9.82464L11.4432 6.21321L10.908 11.2141L6.64955 9.82464Z" fill="#F23035"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L20.5864 14.3719L12.5221 22.8464L24.8449 15.7613Z" fill="#F23035"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.64959 9.82464L10.9081 11.2141L12.5221 22.8463L6.64959 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L10.9081 11.2141L12.5221 22.8463L20.5864 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.82083 31.6843L17.1464 22.3769L40.082 28.6988L45.139 42.5218L19.659 58.5813L5.82083 31.6843Z" fill="#24CC8F"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L17.8892 26.1328L27.7188 28.8422L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.7607 39.0304L37.5482 31.5515L27.7187 28.8422L33.7607 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L27.7187 28.8422L33.7607 39.0304L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.8892 26.1328L17.3782 34.5148L10.1699 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L37.5482 31.5516L33.7607 39.0304L40.9691 41.0173Z" fill="#1CA372"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.3782 34.5148L20.9576 53.79L10.1699 32.5279Z" fill="#1CA372"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L33.7607 39.0304L20.9575 53.79L40.9691 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L33.7607 39.0304L20.9575 53.79L17.3782 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.445 4.15937L16.974 0.420004L25.2227 1.59601L27.5856 6.17539L19.4325 12.8919L13.445 4.15937Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L17.3966 1.6924L20.9318 2.1964L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M23.4779 5.46854L24.4669 2.7004L20.9318 2.1964L23.4779 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L20.9317 2.1964L23.4779 5.46855L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25894L17.3966 1.6924L17.586 4.62854L14.9936 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M26.0704 5.83815L24.4669 2.70041L23.4779 5.46855L26.0704 5.83815Z" fill="#4F2A93"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25893L17.586 4.62853L19.6741 11.1688L14.9936 4.25893Z" fill="#4F2A93"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M26.0703 5.83815L23.4779 5.46855L19.674 11.1688L26.0703 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L23.4779 5.46855L19.674 11.1688L17.586 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7014 16.7775L39.3487 8.36623L52.8984 3.94533L59.9294 9.19883L52.386 25.6769L36.7014 16.7775Z" fill="#50B5E9"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L40.9427 10.0026L46.7498 8.10789L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M53.092 11.2141L52.5568 6.21322L46.7498 8.10789L53.092 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L46.7498 8.10789L53.0919 11.2141L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L40.9427 10.0026L43.4136 14.3719L39.1551 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L52.5568 6.21321L53.092 11.2141L57.3504 9.82464Z" fill="#46A7D9"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L43.4136 14.3719L51.4779 22.8464L39.1551 15.7613Z" fill="#46A7D9"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L53.0919 11.2141L51.4779 22.8463L57.3504 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L53.0919 11.2141L51.4779 22.8463L43.4136 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,5 +1,5 @@
|
||||
<svg width="217" height="48" viewBox="0 0 217 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M108.785 0.0195312C106.343 0.0195312 104.355 1.99967 104.355 4.44184C104.355 6.88401 106.343 8.86415 108.785 8.86415C111.227 8.86415 113.215 6.87668 113.215 4.44184C113.215 2.007 111.227 0.0195312 108.785 0.0195312Z" fill="#FF6165"/>
|
||||
<path d="M148.564 0.0195312C146.121 0.0195312 144.134 1.99967 144.134 4.44184C144.134 6.88401 146.121 8.86415 148.564 8.86415C151.006 8.86415 152.993 6.87668 152.993 4.44184C152.993 2.007 151.006 0.0195312 148.564 0.0195312Z" fill="#50B5E9"/>
|
||||
<path d="M184.2 42.1989C181.332 45.8879 177.005 48 172.319 48C164.355 48 157.776 41.8176 157.344 33.9264C157.322 33.5303 157.322 28.8367 157.322 28.7927C157.322 20.5788 164.047 13.8976 172.319 13.8976C176.411 13.8976 180.379 15.5917 183.195 18.54C184.053 19.4347 184.515 20.6154 184.478 21.8548C184.449 23.0943 183.928 24.2457 183.019 25.0964C181.156 26.8565 178.201 26.7759 176.426 24.9277C175.341 23.7983 173.881 23.1749 172.312 23.1749C169.188 23.1749 166.65 25.6904 166.65 28.7853C166.65 29.2694 166.65 32.995 166.665 33.5083C166.841 36.4052 169.327 38.7154 172.312 38.7154C174.087 38.7154 175.722 37.916 176.8 36.5225C178.369 34.4984 181.31 34.1244 183.342 35.6865C184.332 36.4419 184.962 37.5346 185.124 38.7667C185.285 39.9988 184.948 41.2162 184.192 42.1989H184.2ZM216.82 18.4739V43.4164C216.82 45.9392 214.774 47.9927 212.258 47.9927C210.916 47.9927 209.669 47.3986 208.819 46.4159C206.787 47.45 204.543 47.9927 202.262 47.9927C194.533 47.9927 188.145 41.9129 187.727 34.1464C187.705 33.7577 187.705 29.152 187.705 29.1007C187.705 21.0261 194.239 14.4623 202.262 14.4623C204.419 14.4623 206.545 14.9537 208.503 15.8924C209.332 14.6677 210.726 13.8903 212.266 13.8903C214.781 13.8903 216.827 15.9438 216.827 18.4666L216.82 18.4739ZM207.689 33.721C207.697 33.1196 207.704 29.5774 207.704 29.108C207.704 26.0791 205.262 23.6223 202.262 23.6223C199.263 23.6223 196.821 26.0865 196.821 29.108C196.821 29.5701 196.821 33.2443 196.836 33.7577C197.004 36.5812 199.395 38.84 202.262 38.84C205.13 38.84 207.506 36.5959 207.689 33.721ZM63.3042 18.4739V43.4164C63.3042 45.9392 61.2581 47.9927 58.7426 47.9927C57.4006 47.9927 56.1539 47.3986 55.3032 46.4159C53.2717 47.45 51.0276 47.9927 48.7469 47.9927C41.0099 47.9927 34.6296 41.9129 34.2115 34.1464C34.1895 33.8017 34.1895 30.2008 34.1895 29.1007C34.1895 21.0261 40.7238 14.4623 48.7469 14.4623C50.903 14.4623 53.0371 14.9537 54.9878 15.8924C55.8165 14.6677 57.2099 13.8903 58.75 13.8903C61.2654 13.8903 63.3115 15.9438 63.3115 18.4666L63.3042 18.4739ZM48.7469 23.6223C45.7474 23.6223 43.3053 26.0865 43.3053 29.108C43.3053 29.5847 43.3053 33.237 43.32 33.7503C43.4886 36.5812 45.8794 38.84 48.7469 38.84C51.6143 38.84 53.9904 36.5959 54.1738 33.721C54.1811 33.1196 54.1884 29.5847 54.1884 29.108C54.1884 26.0791 51.7463 23.6223 48.7469 23.6223ZM108.78 14.1396C106.338 14.1396 104.351 16.1931 104.351 18.716V43.4164C104.351 45.9392 106.338 47.9927 108.78 47.9927C111.222 47.9927 113.21 45.9392 113.21 43.4164V18.716C113.21 16.1931 111.222 14.1396 108.78 14.1396ZM148.558 14.1396C146.116 14.1396 144.129 16.1931 144.129 18.716V43.4164C144.129 45.9392 146.116 47.9927 148.558 47.9927C151 47.9927 152.988 45.9392 152.988 43.4164V18.716C152.988 16.1931 151 14.1396 148.558 14.1396ZM98.7551 28.866C98.7551 28.91 98.7551 33.5817 98.7331 33.9704C98.3151 41.8396 91.9275 48 84.1978 48C81.917 48 79.6729 47.45 77.6415 46.4012C76.7908 47.3986 75.5441 48 74.1947 48C71.6792 48 69.6331 45.9245 69.6331 43.3797V4.62032C69.6331 2.07548 71.6792 0 74.1947 0C76.7101 0 78.7562 2.07548 78.7562 4.62032V15.1224C80.487 14.411 82.3351 14.037 84.1978 14.037C92.2282 14.037 98.7551 20.6888 98.7551 28.866ZM84.1978 23.285C81.1983 23.285 78.7562 25.7858 78.7562 28.866C78.7562 29.35 78.7562 33.0536 78.7709 33.5743C78.9469 36.4565 81.3303 38.752 84.1978 38.752C87.0653 38.752 89.434 36.4712 89.6247 33.5523C89.6247 32.929 89.6394 29.328 89.6394 28.866C89.6394 25.7858 87.1973 23.285 84.1978 23.285ZM14.3887 14.037C12.6139 14.037 10.8612 14.3597 9.21109 14.9757V4.62766C9.21109 2.08281 7.14299 0.00733401 4.60554 0.00733401C2.06809 0.00733401 0 2.08281 0 4.62766V43.3797C0 45.9319 2.06809 48 4.60554 48C7.14299 48 9.21109 45.9245 9.21109 43.3797V26.5412C9.40176 26.3358 11.1178 23.285 14.3887 23.285C17.4395 23.285 19.9182 25.7858 19.9182 28.866C19.9182 29.482 19.9182 42.529 19.9036 43.2623C19.8376 45.7852 21.759 47.868 24.2524 47.9927C24.3404 47.9927 24.4211 47.9927 24.5091 47.9927C26.9586 47.9927 28.9753 46.0639 29.1 43.607C29.122 43.2257 29.122 29.0053 29.122 28.866C29.122 20.6888 22.5144 14.037 14.3887 14.037ZM136.399 14.037H133.7V4.62032C133.7 2.07548 131.61 0 129.036 0C126.462 0 124.372 2.07548 124.372 4.62032V14.037H121.673C119.106 14.037 117.009 16.1125 117.009 18.6646C117.009 21.2168 119.099 23.285 121.673 23.285H124.372V43.3797C124.372 45.9319 126.462 48 129.036 48C131.61 48 133.7 45.9245 133.7 43.3797V23.285H136.399C138.973 23.285 141.063 21.2095 141.063 18.6646C141.063 16.1198 138.973 14.037 136.399 14.037Z" fill="white"/>
|
||||
<svg width="145" height="32" viewBox="0 0 145 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M72.7484 0.0131836C71.1152 0.0131836 69.7861 1.33328 69.7861 2.96139C69.7861 4.5895 71.1152 5.90959 72.7484 5.90959C74.3816 5.90959 75.7106 4.58461 75.7106 2.96139C75.7106 1.33816 74.3816 0.0131836 72.7484 0.0131836Z" fill="#FF6165"/>
|
||||
<path d="M99.3498 0.0131836C97.7166 0.0131836 96.3875 1.33328 96.3875 2.96139C96.3875 4.5895 97.7166 5.90959 99.3498 5.90959C100.983 5.90959 102.312 4.58461 102.312 2.96139C102.312 1.33816 100.983 0.0131836 99.3498 0.0131836Z" fill="#50B5E9"/>
|
||||
<path d="M123.181 28.1326C121.263 30.5919 118.37 32 115.236 32C109.91 32 105.511 27.8784 105.221 22.6176C105.207 22.3536 105.207 19.2244 105.207 19.1951C105.207 13.7192 109.704 9.26509 115.236 9.26509C117.973 9.26509 120.626 10.3945 122.509 12.36C123.083 12.9565 123.392 13.7436 123.367 14.5699C123.348 15.3962 122.999 16.1638 122.391 16.7309C121.146 17.9044 119.169 17.8506 117.982 16.6185C117.256 15.8655 116.281 15.45 115.231 15.45C113.142 15.45 111.445 17.127 111.445 19.1902C111.445 19.5129 111.445 21.9966 111.455 22.3389C111.572 24.2701 113.235 25.8102 115.231 25.8102C116.418 25.8102 117.511 25.2773 118.232 24.3484C119.282 22.9989 121.249 22.7496 122.607 23.791C123.269 24.2946 123.691 25.0231 123.799 25.8445C123.907 26.6659 123.681 27.4775 123.176 28.1326H123.181ZM144.995 12.316V28.9442C144.995 30.6261 143.627 31.9951 141.945 31.9951C141.047 31.9951 140.213 31.5991 139.645 30.9439C138.286 31.6333 136.785 31.9951 135.26 31.9951C130.091 31.9951 125.819 27.9419 125.54 22.7642C125.525 22.5051 125.525 19.4347 125.525 19.4005C125.525 14.0174 129.895 9.64156 135.26 9.64156C136.702 9.64156 138.124 9.96914 139.434 10.595C139.988 9.77846 140.92 9.2602 141.95 9.2602C143.632 9.2602 145 10.6292 145 12.3111L144.995 12.316ZM138.889 22.4807C138.894 22.0798 138.899 19.7183 138.899 19.4053C138.899 17.3861 137.266 15.7482 135.26 15.7482C133.254 15.7482 131.621 17.391 131.621 19.4053C131.621 19.7134 131.621 22.1629 131.631 22.5051C131.744 24.3875 133.343 25.8934 135.26 25.8934C137.178 25.8934 138.767 24.3972 138.889 22.4807ZM42.3338 12.316V28.9442C42.3338 30.6261 40.9655 31.9951 39.2833 31.9951C38.3858 31.9951 37.5521 31.5991 36.9832 30.9439C35.6247 31.6333 34.124 31.9951 32.5988 31.9951C27.4247 31.9951 23.158 27.9419 22.8785 22.7642C22.8638 22.5345 22.8638 20.1338 22.8638 19.4005C22.8638 14.0174 27.2335 9.64156 32.5988 9.64156C34.0406 9.64156 35.4678 9.96914 36.7723 10.595C37.3265 9.77846 38.2583 9.2602 39.2882 9.2602C40.9704 9.2602 42.3387 10.6292 42.3387 12.3111L42.3338 12.316ZM32.5988 15.7482C30.5929 15.7482 28.9598 17.391 28.9598 19.4053C28.9598 19.7231 28.9598 22.158 28.9696 22.5002C29.0824 24.3875 30.6812 25.8934 32.5988 25.8934C34.5163 25.8934 36.1053 24.3972 36.2279 22.4807C36.2328 22.0798 36.2377 19.7231 36.2377 19.4053C36.2377 17.3861 34.6046 15.7482 32.5988 15.7482ZM72.7452 9.42643C71.1121 9.42643 69.783 10.7954 69.783 12.4773V28.9442C69.783 30.6261 71.1121 31.9951 72.7452 31.9951C74.3783 31.9951 75.7074 30.6261 75.7074 28.9442V12.4773C75.7074 10.7954 74.3783 9.42643 72.7452 9.42643ZM99.346 9.42643C97.7129 9.42643 96.3838 10.7954 96.3838 12.4773V28.9442C96.3838 30.6261 97.7129 31.9951 99.346 31.9951C100.979 31.9951 102.308 30.6261 102.308 28.9442V12.4773C102.308 10.7954 100.979 9.42643 99.346 9.42643ZM66.041 19.244C66.041 19.2733 66.0411 22.3878 66.0264 22.6469C65.7468 27.893 61.4752 32 56.3061 32C54.7808 32 53.2801 31.6333 51.9216 30.9342C51.3527 31.5991 50.519 32 49.6166 32C47.9344 32 46.5662 30.6164 46.5662 28.9198V3.08021C46.5662 1.38365 47.9344 0 49.6166 0C51.2988 0 52.6671 1.38365 52.6671 3.08021V10.0816C53.8245 9.60734 55.0604 9.35798 56.3061 9.35798C61.6762 9.35798 66.041 13.7925 66.041 19.244ZM56.3061 15.5233C54.3002 15.5233 52.6671 17.1905 52.6671 19.244C52.6671 19.5667 52.6671 22.0358 52.6769 22.3829C52.7946 24.3044 54.3885 25.8347 56.3061 25.8347C58.2236 25.8347 59.8077 24.3141 59.9352 22.3682C59.9352 21.9526 59.945 19.552 59.945 19.244C59.945 17.1905 58.3119 15.5233 56.3061 15.5233ZM9.62221 9.35798C8.43537 9.35798 7.26325 9.57311 6.15978 9.98381V3.0851C6.15978 1.38854 4.77677 0.00488934 3.07989 0.00488934C1.38301 0.00488934 0 1.38854 0 3.0851V28.9198C0 30.6212 1.38301 32 3.07989 32C4.77677 32 6.15978 30.6164 6.15978 28.9198V17.6941C6.28729 17.5572 7.4349 15.5233 9.62221 15.5233C11.6624 15.5233 13.32 17.1905 13.32 19.244C13.32 19.6547 13.32 28.3526 13.3102 28.8416C13.2661 30.5235 14.551 31.912 16.2185 31.9951C16.2773 31.9951 16.3313 31.9951 16.3901 31.9951C18.0281 31.9951 19.3768 30.7092 19.4602 29.0714C19.4749 28.8171 19.4749 19.3369 19.4749 19.244C19.4749 13.7925 15.0561 9.35798 9.62221 9.35798ZM91.2147 9.35798H89.41V3.08021C89.41 1.38365 88.0122 0 86.2908 0C84.5694 0 83.1717 1.38365 83.1717 3.08021V9.35798H81.3669C79.6504 9.35798 78.2478 10.7416 78.2478 12.4431C78.2478 14.1445 79.6455 15.5233 81.3669 15.5233H83.1717V28.9198C83.1717 30.6212 84.5694 32 86.2908 32C88.0122 32 89.41 30.6164 89.41 28.9198V15.5233H91.2147C92.9361 15.5233 94.3339 14.1396 94.3339 12.4431C94.3339 10.7465 92.9361 9.35798 91.2147 9.35798Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="330" height="80" viewBox="0 0 330 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.797 60.5502C165.534 61.8466 171.631 62.7536 178.221 63.1767C208.94 65.1492 233.733 56.6838 260.68 47.483C282.33 40.091 305.37 32.2243 333.99 28.9144L336 16.3018C260.81 7.08865 233.373 23.1672 205.362 39.5825C192.037 47.3908 178.583 55.2753 159.797 60.5502Z" fill="#BDA8FF"/>
|
||||
<path d="M0 80L331.948 79.9998V29.1594C268.976 36.9871 233.03 66.6959 178.221 63.1767C112.951 58.9858 95.9516 7.31934 0.000104656 0L0 80Z" fill="#925CF3"/>
|
||||
<path d="M203.54 40.6496C166.339 36.8525 141.531 39.6251 122.334 45.4666C133.94 51.8989 145.792 57.3851 159.797 60.5502C177.727 55.5155 190.801 48.1036 203.54 40.6496Z" fill="#D5C8FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 803 B |
@@ -117,7 +117,7 @@ export default {
|
||||
closeWithAction () {
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
this.$router.push({ name: 'achievements' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||
}, 200);
|
||||
},
|
||||
},
|
||||
|
||||